Exemple #1
0
    def create(self, namespace, pod_name, docker_id):
        """"Create a pod."""
        self.pod_name = pod_name
        self.docker_id = docker_id
        self.namespace = namespace
        self.policy_parser = PolicyParser(self.namespace)
        logger.info('Configuring pod %s/%s (container_id %s)', self.namespace,
                    self.pod_name, self.docker_id)

        # Obtain information from Docker Client and validate container state.
        # If validation fails, the plugin will exit.
        self._validate_container_state(self.docker_id)

        try:
            endpoint = self._configure_interface()
            logger.info("Created Calico endpoint: %s", endpoint.endpoint_id)
            self._configure_profile(endpoint)
        except BaseException:
            # Check to see if an endpoint has been created.  If so,
            # we need to tear down any state we may have created.
            logger.exception("Error networking pod - cleaning up")

            try:
                self.delete(namespace, pod_name, docker_id)
            except BaseException:
                # Catch all errors tearing down the pod - this
                # is best-effort.
                logger.exception("Error cleaning up pod")

            # We've torn down, exit.
            logger.info("Done cleaning up")
            sys.exit(1)
        else:
            logger.info("Successfully configured networking for pod %s/%s",
                        self.namespace, self.pod_name)
Exemple #2
0
    def __init__(self, observations):
        # encode observation data as int values
        self.state_action_encoder = StateActionEncoder(observations)
        self.state_action_encoder.observations_to_int()
        dimensions = self.state_action_encoder.parse_dimensions()

        # create reward, transition, and policy parsers
        self.reward_parser = RewardParser(observations, dimensions)
        self.transition_parser = TransitionParser(observations, dimensions)
        self.policy_parser = PolicyParser(dimensions)
Exemple #3
0
class MarkovAgent:
    def __init__(self, observations):
        # encode observation data as int values
        self.state_action_encoder = StateActionEncoder(observations)
        # encoding json coded data -> int
        self.state_action_encoder.observations_to_int()
        dimensions = self.state_action_encoder.parse_dimensions()
        print dimensions

        # create reward, transition, and policy parsers
        self.reward_parser = RewardParser(observations, dimensions)
        self.transition_parser = TransitionParser(observations, dimensions)
        self.policy_parser = PolicyParser(dimensions)

    def learn(self):
        # calculating average rewards of each state
        # average of each state = total rewards of each state / total visits of each state
        R = self.reward_parser.rewards()
        # calculating the probability of each state->action
        # probablities of each state->action = transition counts of each state->action / total transitions
        P = self.transition_parser.transition_probabilities()

        # learn int-encoded policy and convert to readable dictionary
        encoded_policy = self.policy_parser.policy(P, R)
        self.policy = self.state_action_encoder.parse_encoded_policy(
            encoded_policy)
    def create(self, namespace, pod_name, docker_id):
        """"Create a pod."""
        self.pod_name = pod_name
        self.docker_id = docker_id
        self.namespace = namespace
        self.policy_parser = PolicyParser(self.namespace)
        logger.info('Configuring pod %s/%s (container_id %s)',
                    self.namespace, self.pod_name, self.docker_id)

        # Obtain information from Docker Client and validate container state.
        # If validation fails, the plugin will exit.
        self._validate_container_state(self.docker_id)

        try:
            endpoint = self._configure_interface()
            logger.info("Created Calico endpoint: %s", endpoint.endpoint_id)
            self._configure_profile(endpoint)
        except BaseException:
            # Check to see if an endpoint has been created.  If so,
            # we need to tear down any state we may have created.
            logger.exception("Error networking pod - cleaning up")

            try:
                self.delete(namespace, pod_name, docker_id)
            except BaseException:
                # Catch all errors tearing down the pod - this
                # is best-effort.
                logger.exception("Error cleaning up pod")

            # We've torn down, exit. 
            logger.info("Done cleaning up")
            sys.exit(1)
        else:
            logger.info("Successfully configured networking for pod %s/%s",
                        self.namespace, self.pod_name)
Exemple #5
0
  def __init__(self, observations):
    # encode observation data as int values
    self.state_action_encoder = StateActionEncoder(observations)
    self.state_action_encoder.observations_to_int()
    dimensions = self.state_action_encoder.parse_dimensions()

    # create reward, transition, and policy parsers
    self.reward_parser = RewardParser(observations, dimensions)
    self.transition_parser = TransitionParser(observations, dimensions)
    self.policy_parser = PolicyParser(dimensions)
Exemple #6
0
    def create(self, namespace, pod_name, docker_id):
        """"Create a pod."""
        self.pod_name = pod_name
        self.docker_id = docker_id
        self.namespace = namespace
        self.policy_parser = PolicyParser(self.namespace)
        self.profile_name = "%s_%s_%s" % (self.namespace,
                                          self.pod_name,
                                          str(self.docker_id)[:12])
        logger.info('Configuring pod %s/%s (container_id %s)',
                    self.namespace, self.pod_name, self.docker_id)

        try:
            endpoint = self._configure_interface()
            logger.info("Created Calico endpoint: %s", endpoint.endpoint_id)
            self._configure_profile(endpoint)
        except CalledProcessError as e:
            logger.error('Error code %d creating pod networking: %s\n%s',
                         e.returncode, e.output, e)
            sys.exit(1)
        logger.info("Successfully configured networking for pod %s/%s",
                    self.namespace, self.pod_name)
    def create(self, namespace, pod_name, docker_id):
        """"Create a pod."""
        self.pod_name = pod_name
        self.docker_id = docker_id
        self.namespace = namespace
        self.policy_parser = PolicyParser(self.namespace)
        self.profile_name = "%s_%s_%s" % (self.namespace, self.pod_name, str(self.docker_id)[:12])

        logger.info("Configuring docker container %s", self.docker_id)

        try:
            endpoint = self._configure_interface()
            self._configure_profile(endpoint)
        except CalledProcessError as e:
            logger.error("Error code %d creating pod networking: %s\n%s", e.returncode, e.output, e)
            sys.exit(1)
Exemple #8
0
class MarkovAgent:
  def __init__(self, observations):
    # encode observation data as int values
    self.state_action_encoder = StateActionEncoder(observations)
    self.state_action_encoder.observations_to_int()
    dimensions = self.state_action_encoder.parse_dimensions()

    # create reward, transition, and policy parsers
    self.reward_parser = RewardParser(observations, dimensions)
    self.transition_parser = TransitionParser(observations, dimensions)
    self.policy_parser = PolicyParser(dimensions)

  def learn(self):
    R = self.reward_parser.rewards()
    P = self.transition_parser.transition_probabilities()

    # learn int-encoded policy and convert to readable dictionary
    encoded_policy = self.policy_parser.policy(P, R)
    self.policy = self.state_action_encoder.parse_encoded_policy(encoded_policy)
Exemple #9
0
class MarkovAgent:
    def __init__(self, observations):
        # encode observation data as int values
        self.state_action_encoder = StateActionEncoder(observations)
        self.state_action_encoder.observations_to_int()
        dimensions = self.state_action_encoder.parse_dimensions()

        # create reward, transition, and policy parsers
        self.reward_parser = RewardParser(observations, dimensions)
        self.transition_parser = TransitionParser(observations, dimensions)
        self.policy_parser = PolicyParser(dimensions)

    def learn(self):
        R = self.reward_parser.rewards()
        P = self.transition_parser.transition_probabilities()

        # learn int-encoded policy and convert to readable dictionary
        encoded_policy = self.policy_parser.policy(P, R)
        self.policy = self.state_action_encoder.parse_encoded_policy(
            encoded_policy)
class NetworkPlugin(object):
    def __init__(self):
        self.pod_name = None
        self.profile_name = None
        self.namespace = None
        self.docker_id = None
        self.auth_token = os.environ.get("KUBE_AUTH_TOKEN", None)
        self.policy_parser = None

        self._datastore_client = IPAMClient()
        self._docker_client = Client(
            version=DOCKER_VERSION, base_url=os.getenv("DOCKER_HOST", "unix://var/run/docker.sock")
        )

    def create(self, namespace, pod_name, docker_id):
        """"Create a pod."""
        self.pod_name = pod_name
        self.docker_id = docker_id
        self.namespace = namespace
        self.policy_parser = PolicyParser(self.namespace)
        self.profile_name = "%s_%s_%s" % (self.namespace, self.pod_name, str(self.docker_id)[:12])

        logger.info("Configuring docker container %s", self.docker_id)

        try:
            endpoint = self._configure_interface()
            self._configure_profile(endpoint)
        except CalledProcessError as e:
            logger.error("Error code %d creating pod networking: %s\n%s", e.returncode, e.output, e)
            sys.exit(1)

    def delete(self, namespace, pod_name, docker_id):
        """Cleanup after a pod."""
        self.pod_name = pod_name
        self.docker_id = docker_id
        self.namespace = namespace
        self.profile_name = "%s_%s_%s" % (self.namespace, self.pod_name, str(self.docker_id)[:12])

        logger.info("Deleting container %s with profile %s", self.docker_id, self.profile_name)

        # Remove the profile for the workload.
        self._container_remove()

        # Delete profile
        try:
            self._datastore_client.remove_profile(self.profile_name)
        except:
            logger.warning("Cannot remove profile %s; Profile cannot " "be found.", self.profile_name)

    def status(self, namespace, pod_name, docker_id):
        self.namespace = namespace
        self.pod_name = pod_name
        self.docker_id = docker_id

        # Find the endpoint
        try:
            endpoint = self._datastore_client.get_endpoint(
                hostname=HOSTNAME, orchestrator_id=ORCHESTRATOR_ID, workload_id=self.docker_id
            )
        except KeyError:
            logger.error("Container %s doesn't contain any endpoints", self.docker_id)
            sys.exit(1)

        # Retrieve IPAddress from the attached IPNetworks on the endpoint
        # Since Kubernetes only supports ipv4, we'll only check for ipv4 nets
        if not endpoint.ipv4_nets:
            logger.error("Exiting. No IPs attached to endpoint %s", self.docker_id)
            sys.exit(1)
        else:
            ip_net = list(endpoint.ipv4_nets)
            if len(ip_net) is not 1:
                logger.warning("There is more than one IPNetwork attached " "to endpoint %s", self.docker_id)
            ip = ip_net[0].ip

        logger.info("Retrieved pod IP Address: %s", ip)

        json_dict = {"apiVersion": "v1beta1", "kind": "PodNetworkStatus", "ip": str(ip)}

        logger.debug("Writing status to stdout: \n%s", json.dumps(json_dict))
        print(json.dumps(json_dict))

    def _configure_profile(self, endpoint):
        """
        Configure the calico profile on the given endpoint.
        """
        pod = self._get_pod_config()

        logger.info("Configuring Pod Profile: %s", self.profile_name)

        if self._datastore_client.profile_exists(self.profile_name):
            logger.error("Profile with name %s already exists, exiting.", self.profile_name)
            sys.exit(1)
        else:
            rules = self._generate_rules(pod)
            self._datastore_client.create_profile(self.profile_name, rules)

        # Add tags to the profile.
        self._apply_tags(pod)

        # Set the profile for the workload.
        logger.info("Setting profile %s on endpoint %s", self.profile_name, endpoint.endpoint_id)
        self._datastore_client.set_profiles_on_endpoint([self.profile_name], endpoint_id=endpoint.endpoint_id)
        logger.info("Finished configuring profile.")

    def _configure_interface(self):
        """Configure the Calico interface for a pod.

        This involves the following steps:
        1) Determine the IP that docker assigned to the interface inside the
           container
        2) Delete the docker-assigned veth pair that's attached to the docker
           bridge
        3) Create a new calico veth pair, using the docker-assigned IP for the
           end in the container's namespace
        4) Assign the node's IP to the host end of the veth pair (required for
           compatibility with kube-proxy REDIRECT iptables rules).
        """
        # Set up parameters
        container_pid = self._get_container_pid(self.docker_id)
        interface = "eth0"

        self._delete_docker_interface()
        logger.info("Configuring Calico network interface")
        ep = self._container_add(container_pid, interface)

        # Log our container's interfaces after adding the new interface.
        _log_interfaces(container_pid)

        interface_name = generate_cali_interface_name(IF_PREFIX, ep.endpoint_id)
        node_ip = self._get_node_ip()
        logger.info("Adding IP %s to interface %s", node_ip, interface_name)

        # This is slightly tricky. Since the kube-proxy sometimes
        # programs REDIRECT iptables rules, we MUST have an IP on the host end
        # of the caliXXX veth pairs. This is because the REDIRECT rule
        # rewrites the destination ip/port of traffic from a pod to a service
        # VIP. The destination port is rewriten to an arbitrary high-numbered
        # port, and the destination IP is rewritten to one of the IPs allocated
        # to the interface. This fails if the interface doesn't have an IP,
        # so we allocate an IP which is already allocated to the node. We set
        # the subnet to /32 so that the routing table is not affected;
        # no traffic for the node_ip's subnet will use the /32 route.
        check_call(["ip", "addr", "add", node_ip + "/32", "dev", interface_name])
        logger.info("Finished configuring network interface")
        return ep

    def _container_add(self, pid, interface):
        """
        Add a container (on this host) to Calico networking with the given IP.
        """
        # Check if the container already exists. If it does, exit.
        try:
            _ = self._datastore_client.get_endpoint(
                hostname=HOSTNAME, orchestrator_id=ORCHESTRATOR_ID, workload_id=self.docker_id
            )
        except KeyError:
            # Calico doesn't know about this container.  Continue.
            pass
        else:
            logger.error("This container has already been configured " "with Calico Networking.")
            sys.exit(1)

        # Obtain information from Docker Client and validate container state
        self._validate_container_state(self.docker_id)

        ip_list = [self._assign_container_ip()]

        # Create Endpoint object
        try:
            logger.info("Creating endpoint with IPs %s", ip_list)
            ep = self._datastore_client.create_endpoint(HOSTNAME, ORCHESTRATOR_ID, self.docker_id, ip_list)
        except (AddrFormatError, KeyError):
            logger.exception("Failed to create endpoint with IPs %s. " "Unassigning IP address, then exiting.", ip_list)
            self._datastore_client.release_ips(set(ip_list))
            sys.exit(1)

        # Create the veth, move into the container namespace, add the IP and
        # set up the default routes.
        logger.info("Creating the veth with namespace pid %s on interface " "name %s", pid, interface)
        ep.mac = ep.provision_veth(netns.PidNamespace(pid), interface)

        logger.info("Setting mac address %s to endpoint %s", ep.mac, ep.name)
        self._datastore_client.set_endpoint(ep)

        # Let the caller know what endpoint was created.
        return ep

    def _assign_container_ip(self):
        """
        Assign IPAddress either with the assigned docker IPAddress or utilize
        calico IPAM.

        The option to utilize IPAM is indicated by the environment variable
        "CALICO_IPAM".
        True indicates to utilize Calico's auto_assign IPAM policy.
        False indicate to utilize the docker assigned IPAddress

        :return IPAddress which has been assigned
        """

        def _assign(ip):
            """
            Local helper function for assigning an IP and checking for errors.
            Only used when operating with CALICO_IPAM=false
            """
            try:
                logger.info("Attempting to assign IP %s", ip)
                self._datastore_client.assign_ip(ip, str(self.docker_id), None)
            except (ValueError, RuntimeError):
                logger.exception("Failed to assign IPAddress %s", ip)
                sys.exit(1)

        if CALICO_IPAM == "true":
            logger.info("Using Calico IPAM")
            try:
                ipv4s, ipv6s = self._datastore_client.auto_assign_ips(1, 0, self.docker_id, None)
                ip = ipv4s[0]
                logger.debug("IPAM assigned ipv4=%s; ipv6= %s", ipv4s, ipv6s)
            except RuntimeError as err:
                logger.error("Cannot auto assign IPAddress: %s", err.message)
                sys.exit(1)
        else:
            logger.info("Using docker assigned IP address")
            ip = self._read_docker_ip()

            try:
                # Try to assign the address using the _assign helper function.
                _assign(ip)
            except AlreadyAssignedError:
                # If the Docker IP is already assigned, it is most likely that
                # an endpoint has been removed under our feet.  When using
                # Docker IPAM, treat Docker as the source of
                # truth for IP addresses.
                logger.warning("Docker IP is already assigned, finding " "stale endpoint")
                self._datastore_client.release_ips(set([ip]))

                # Clean up whatever existing endpoint has this IP address.
                # We can improve this later by making use of IPAM attributes
                # in libcalico to store the endpoint ID.  For now,
                # just loop through endpoints on this host.
                endpoints = self._datastore_client.get_endpoints(hostname=HOSTNAME, orchestrator_id=ORCHESTRATOR_ID)
                for ep in endpoints:
                    if IPNetwork(ip) in ep.ipv4_nets:
                        logger.warning("Deleting stale endpoint %s", ep.endpoint_id)
                        for profile_id in ep.profile_ids:
                            self._datastore_client.remove_profile(profile_id)
                        self._datastore_client.remove_endpoint(ep)
                        break

                # Assign the IP address to the new endpoint.  It shouldn't
                # be assigned, since we just unassigned it.
                logger.warning("Retry Docker assigned IP")
                _assign(ip)
        return ip

    def _container_remove(self):
        """
        Remove the indicated container on this host from Calico networking
        """
        # Find the endpoint ID. We need this to find any ACL rules
        try:
            endpoint = self._datastore_client.get_endpoint(
                hostname=HOSTNAME, orchestrator_id=ORCHESTRATOR_ID, workload_id=self.docker_id
            )
        except KeyError:
            logger.exception("Container %s doesn't contain any endpoints", self.docker_id)
            sys.exit(1)

        # Remove any IP address assignments that this endpoint has
        ip_set = set()
        for net in endpoint.ipv4_nets | endpoint.ipv6_nets:
            ip_set.add(net.ip)
        logger.info("Removing IP addresses %s from endpoint %s", ip_set, endpoint.name)
        self._datastore_client.release_ips(ip_set)

        # Remove the veth interface from endpoint
        logger.info("Removing veth interface from endpoint %s", endpoint.name)
        try:
            netns.remove_veth(endpoint.name)
        except CalledProcessError:
            logger.exception("Could not remove veth interface from " "endpoint %s", endpoint.name)

        # Remove the container/endpoint from the datastore.
        try:
            self._datastore_client.remove_workload(HOSTNAME, ORCHESTRATOR_ID, self.docker_id)
            logger.info("Successfully removed workload from datastore")
        except KeyError:
            logger.exception("Failed to remove workload.")

        logger.info("Removed Calico interface from %s", self.docker_id)

    def _validate_container_state(self, container_name):
        info = self._get_container_info(container_name)

        # Check the container is actually running.
        if not info["State"]["Running"]:
            logger.error("The container is not currently running.")
            sys.exit(1)

        # We can't set up Calico if the container shares the host namespace.
        if info["HostConfig"]["NetworkMode"] == "host":
            logger.error("Can't add the container to Calico because " "it is running NetworkMode = host.")
            sys.exit(1)

    def _get_container_info(self, container_name):
        try:
            info = self._docker_client.inspect_container(container_name)
        except APIError as e:
            if e.response.status_code == 404:
                logger.error("Container %s was not found. Exiting.", container_name)
            else:
                logger.error(e.message)
            sys.exit(1)
        return info

    def _get_container_pid(self, container_name):
        return self._get_container_info(container_name)["State"]["Pid"]

    def _read_docker_ip(self):
        """Get the IP for the pod's infra container."""
        container_info = self._get_container_info(self.docker_id)
        ip = container_info["NetworkSettings"]["IPAddress"]
        logger.info("Docker-assigned IP is %s", ip)
        return IPAddress(ip)

    def _get_node_ip(self):
        """
        Determine the IP for the host node.
        """
        # Compile list of addresses on network, return the first entry.
        # Try IPv4 and IPv6.
        addrs = get_host_ips(version=4) or get_host_ips(version=6)

        try:
            addr = addrs[0]
            logger.info("Using IP Address %s", addr)
            return addr
        except IndexError:
            # If both get_host_ips return empty lists, print message and exit.
            logger.exception(
                "No Valid IP Address Found for Host - cannot " "configure networking for pod %s. " "Exiting",
                self.pod_name,
            )
            sys.exit(1)

    def _delete_docker_interface(self):
        """Delete the existing veth connecting to the docker bridge."""
        logger.info("Deleting docker interface eth0")

        # Get the PID of the container.
        pid = str(self._get_container_pid(self.docker_id))
        logger.info("Container %s running with PID %s", self.docker_id, pid)

        # Set up a link to the container's netns.
        logger.info("Linking to container's netns")
        logger.debug(check_output(["mkdir", "-p", "/var/run/netns"]))
        netns_file = "/var/run/netns/" + pid
        if not os.path.isfile(netns_file):
            logger.debug(check_output(["ln", "-s", "/proc/" + pid + "/ns/net", netns_file]))

        # Log our container's interfaces before making any changes.
        _log_interfaces(pid)

        # Reach into the netns and delete the docker-allocated interface.
        logger.debug(check_output(["ip", "netns", "exec", pid, "ip", "link", "del", "eth0"]))

        # Log our container's interfaces after making our changes.
        _log_interfaces(pid)

        # Clean up after ourselves (don't want to leak netns files)
        logger.debug(check_output(["rm", netns_file]))

    def _get_pod_ports(self, pod):
        """
        Get the list of ports on containers in the Pod.

        :return list ports: the Kubernetes ContainerPort objects for the pod.
        """
        ports = []
        for container in pod["spec"]["containers"]:
            try:
                more_ports = container["ports"]
                logger.info("Adding ports %s", more_ports)
                ports.extend(more_ports)
            except KeyError:
                pass
        return ports

    def _get_pod_config(self):
        """Get the list of pods from the Kube API server."""
        pods = self._get_api_path("pods")
        logger.debug("Got pods %s", pods)

        for pod in pods:
            logger.debug("Processing pod %s", pod)
            ns = pod["metadata"]["namespace"].replace("/", "_")
            name = pod["metadata"]["name"].replace("/", "_")
            if ns == self.namespace and name == self.pod_name:
                this_pod = pod
                break
        else:
            raise KeyError("Pod not found: " + self.pod_name)
        logger.debug("Got pod data %s", this_pod)
        return this_pod

    def _get_api_path(self, path):
        """Get a resource from the API specified API path.

        e.g.
        _get_api_path('pods')

        :param path: The relative path to an API endpoint.
        :return: A list of JSON API objects
        :rtype list
        """
        logger.info("Getting API Resource: %s from KUBE_API_ROOT: %s", path, KUBE_API_ROOT)
        session = requests.Session()
        if self.auth_token:
            session.headers.update({"Authorization": "Bearer " + self.auth_token})
        response = session.get(KUBE_API_ROOT + path, verify=False)
        response_body = response.text

        # The response body contains some metadata, and the pods themselves
        # under the 'items' key.
        return json.loads(response_body)["items"]

    def _api_root_secure(self):
        """
        Checks whether the KUBE_API_ROOT is secure or insecure.
        If not an http or https address, exit.

        :return: Boolean: True if secure. False if insecure
        """
        if KUBE_API_ROOT[:5] == "https":
            return True
        elif KUBE_API_ROOT[:5] == "http:":
            return False
        else:
            logger.error(
                "KUBE_API_ROOT is not set correctly (%s). " "Please specify as http or https address. Exiting",
                KUBE_API_ROOT,
            )
            sys.exit(1)

    def _generate_rules(self, pod):
        """
        Generate Rules takes human readable policy strings in annotations
        and returns a libcalico Rules object.

        :return tuple of inbound_rules, outbound_rules
        """
        # Create allow and per-namespace rules for later use.
        allow = Rule(action="allow")
        allow_ns = Rule(action="allow", src_tag=self._get_namespace_tag(pod))
        annotations = self._get_metadata(pod, "annotations")
        logger.debug("Found annotations: %s", annotations)

        if self.namespace == "kube-system":
            # Pods in the kube-system namespace must be accessible by all
            # other pods for services like DNS to work.
            logger.info("Pod is in kube-system namespace - allow all")
            inbound_rules = [allow]
            outbound_rules = [allow]
        elif annotations and POLICY_ANNOTATION_KEY in annotations:
            # If policy annotations are defined, use them to generate rules.
            logger.info("Generating advanced security policy from annotations")
            rules = annotations[POLICY_ANNOTATION_KEY]
            inbound_rules = []
            outbound_rules = [allow]
            for rule in rules.split(";"):
                parsed_rule = self.policy_parser.parse_line(rule)
                inbound_rules.append(parsed_rule)
        else:
            # If not annotations are defined, just use the configured
            # default policy.
            if DEFAULT_POLICY == "ns_isolation":
                # Isolate on namespace boundaries by default.
                logger.debug("Default policy is namespace isolation")
                inbound_rules = [allow_ns]
                outbound_rules = [allow]
            else:
                # Allow all traffic by default.
                logger.debug("Default policy is allow all")
                inbound_rules = [allow]
                outbound_rules = [allow]

        return Rules(id=self.profile_name, inbound_rules=inbound_rules, outbound_rules=outbound_rules)

    def _apply_tags(self, pod):
        """
        In addition to Calico's default pod_name tag,
        Add tags generated from Kubernetes Labels and Namespace
            Ex. labels: {key:value} -> tags+= namespace_key_value
        Add tag for namespace
            Ex. namespace: default -> tags+= namespace_default

        :param self.profile_name: The name of the Calico profile.
        :type self.profile_name: string
        :param pod: The config dictionary for the pod being created.
        :type pod: dict
        :return:
        """
        logger.info("Applying tags")

        try:
            profile = self._datastore_client.get_profile(self.profile_name)
        except KeyError:
            logger.error("Could not apply tags. Profile %s could not be " "found. Exiting", self.profile_name)
            sys.exit(1)

        # Grab namespace and create a tag if it exists.
        ns_tag = self._get_namespace_tag(pod)
        logger.info("Adding tag %s", ns_tag)
        profile.tags.add(ns_tag)

        # Create tags from labels
        labels = self._get_metadata(pod, "labels")
        if labels:
            for k, v in labels.iteritems():
                tag = self._label_to_tag(k, v)
                logger.info("Adding tag %s", tag)
                profile.tags.add(tag)
        else:
            logger.warning("No labels found in pod %s", pod)

        self._datastore_client.profile_update_tags(profile)

        logger.info("Finished applying tags.")

    def _get_metadata(self, pod, key):
        """
        Return Metadata[key] Object given Pod
        Returns None if no key-value exists
        """
        try:
            val = pod["metadata"][key]
        except (KeyError, TypeError):
            logger.warning("No %s found in pod %s", key, pod)
            return None

        logger.debug("Pod %s: %s", key, val)
        return val

    def _escape_chars(self, unescaped_string):
        """
        Calico can only handle 3 special chars, '_.-'
        This function uses regex sub to replace SCs with '_'
        """
        # Character to replace symbols
        swap_char = "_"

        # If swap_char is in string, double it.
        unescaped_string = re.sub(swap_char, "%s%s" % (swap_char, swap_char), unescaped_string)

        # Substitute all invalid chars.
        return re.sub("[^a-zA-Z0-9\.\_\-]", swap_char, unescaped_string)

    def _get_namespace_tag(self, pod):
        """
        Pull metadata for namespace and return it and a generated NS tag
        """
        assert self.namespace
        ns_tag = self._escape_chars("%s=%s" % ("namespace", self.namespace))
        return ns_tag

    def _label_to_tag(self, label_key, label_value):
        """
        Labels are key-value pairs, tags are single strings. This function
        handles that translation.
        1) Concatenate key and value with '='
        2) Prepend a pod's namespace followed by '/' if available
        3) Escape the generated string so it is Calico compatible
        :param label_key: key to label
        :param label_value: value to given key for a label
        :param namespace: Namespace string, input None if not available
        :param types: (self, string, string, string)
        :return single string tag
        :rtype string
        """
        tag = "%s=%s" % (label_key, label_value)
        tag = "%s/%s" % (self.namespace, tag)
        tag = self._escape_chars(tag)
        return tag
Exemple #11
0
class NetworkPlugin(object):

    def __init__(self, config):
        self.pod_name = None
        self.profile_name = None
        self.namespace = None
        self.docker_id = None
        self.policy_parser = None

        # Get configuration from the given dictionary.
        logger.debug("Plugin running with config: %s", config)
        self.auth_token = config[KUBE_AUTH_TOKEN_VAR]
        self.api_root = config[KUBE_API_ROOT_VAR]
        self.calico_ipam = config[CALICO_IPAM_VAR].lower()
        self.default_policy = config[DEFAULT_POLICY_VAR].lower()

        self._datastore_client = IPAMClient()
        self._docker_client = Client(
            version=DOCKER_VERSION,
            base_url=os.getenv("DOCKER_HOST", "unix://var/run/docker.sock"))

    def create(self, namespace, pod_name, docker_id):
        """"Create a pod."""
        self.pod_name = pod_name
        self.docker_id = docker_id
        self.namespace = namespace
        self.policy_parser = PolicyParser(self.namespace)
        self.profile_name = "%s_%s_%s" % (self.namespace,
                                          self.pod_name,
                                          str(self.docker_id)[:12])
        logger.info('Configuring pod %s/%s (container_id %s)',
                    self.namespace, self.pod_name, self.docker_id)

        try:
            endpoint = self._configure_interface()
            logger.info("Created Calico endpoint: %s", endpoint.endpoint_id)
            self._configure_profile(endpoint)
        except CalledProcessError as e:
            logger.error('Error code %d creating pod networking: %s\n%s',
                         e.returncode, e.output, e)
            sys.exit(1)
        logger.info("Successfully configured networking for pod %s/%s",
                    self.namespace, self.pod_name)

    def delete(self, namespace, pod_name, docker_id):
        """Cleanup after a pod."""
        self.pod_name = pod_name
        self.docker_id = docker_id
        self.namespace = namespace
        self.profile_name = "%s_%s_%s" % (self.namespace,
                                          self.pod_name,
                                          str(self.docker_id)[:12])

        logger.info('Removing networking from pod %s/%s (container id %s)',
                    self.namespace, self.pod_name, self.docker_id)

        # Remove the profile for the workload.
        self._container_remove()

        # Delete profile
        try:
            logger.info("Deleting Calico profile: %s", self.profile_name)
            self._datastore_client.remove_profile(self.profile_name)
        except:
            logger.warning("Cannot remove profile %s; Profile cannot "
                           "be found.", self.profile_name)
        logger.info("Successfully removed networking for pod %s/%s",
                    self.namespace, self.pod_name)

    def status(self, namespace, pod_name, docker_id):
        self.namespace = namespace
        self.pod_name = pod_name
        self.docker_id = docker_id

        if self._uses_host_networking(self.docker_id):
            # We don't perform networking / assign IP addresses for pods running
            # in the host namespace, and so we can't return a status update
            # for them.
            logger.debug("Ignoring status for pod %s/%s in host namespace",
                         self.namespace, self.pod_name)
            sys.exit(0)

        # Find the endpoint
        try:
            endpoint = self._datastore_client.get_endpoint(
                hostname=HOSTNAME,
                orchestrator_id=ORCHESTRATOR_ID,
                workload_id=self.docker_id
            )
        except KeyError:
            logger.error("Error in status: No endpoint for pod: %s/%s",
                         self.namespace, self.pod_name)
            sys.exit(1)

        # Retrieve IPAddress from the attached IPNetworks on the endpoint
        # Since Kubernetes only supports ipv4, we'll only check for ipv4 nets
        if not endpoint.ipv4_nets:
            logger.error("Error in status: No IPs attached to pod %s/%s",
                         self.namespace, self.pod_name)
            sys.exit(1)
        else:
            ip_net = list(endpoint.ipv4_nets)
            if len(ip_net) is not 1:
                logger.warning("There is more than one IPNetwork attached "
                               "to pod %s/%s", self.namespace, self.pod_name)
            ip = ip_net[0].ip

        logger.debug("Retrieved pod IP Address: %s", ip)

        json_dict = {
            "apiVersion": "v1beta1",
            "kind": "PodNetworkStatus",
            "ip": str(ip)
        }

        logger.debug("Writing status to stdout: \n%s", json.dumps(json_dict))
        print(json.dumps(json_dict))

    def _configure_profile(self, endpoint):
        """
        Configure the calico profile on the given endpoint.
        """
        pod = self._get_pod_config()

        logger.info('Configuring Pod Profile: %s', self.profile_name)

        if self._datastore_client.profile_exists(self.profile_name):
            logger.error("Profile with name %s already exists, exiting.",
                         self.profile_name)
            sys.exit(1)
        else:
            rules = self._generate_rules(pod)
            self._datastore_client.create_profile(self.profile_name, rules)

        # Add tags to the profile.
        self._apply_tags(pod)

        # Set the profile for the workload.
        logger.info('Setting profile %s on endpoint %s',
                    self.profile_name, endpoint.endpoint_id)
        self._datastore_client.set_profiles_on_endpoint(
            [self.profile_name], endpoint_id=endpoint.endpoint_id
        )
        logger.debug('Finished configuring profile.')

    def _configure_interface(self):
        """Configure the Calico interface for a pod.

        This involves the following steps:
        1) Determine the IP that docker assigned to the interface inside the
           container
        2) Delete the docker-assigned veth pair that's attached to the docker
           bridge
        3) Create a new calico veth pair, using the docker-assigned IP for the
           end in the container's namespace
        4) Assign the node's IP to the host end of the veth pair (required for
           compatibility with kube-proxy REDIRECT iptables rules).
        """
        # Set up parameters
        container_pid = self._get_container_pid(self.docker_id)
        interface = 'eth0'

        self._delete_docker_interface()
        logger.info('Configuring Calico network interface')
        ep = self._container_add(container_pid, interface)

        # Log our container's interfaces after adding the new interface.
        _log_interfaces(container_pid)

        interface_name = generate_cali_interface_name(IF_PREFIX,
                                                      ep.endpoint_id)
        node_ip = self._get_node_ip()
        logger.debug('Adding node IP %s to host-side veth %s', node_ip, interface_name)

        # This is slightly tricky. Since the kube-proxy sometimes
        # programs REDIRECT iptables rules, we MUST have an IP on the host end
        # of the caliXXX veth pairs. This is because the REDIRECT rule
        # rewrites the destination ip/port of traffic from a pod to a service
        # VIP. The destination port is rewriten to an arbitrary high-numbered
        # port, and the destination IP is rewritten to one of the IPs allocated
        # to the interface. This fails if the interface doesn't have an IP,
        # so we allocate an IP which is already allocated to the node. We set
        # the subnet to /32 so that the routing table is not affected;
        # no traffic for the node_ip's subnet will use the /32 route.
        check_call(['ip', 'addr', 'add', node_ip + '/32',
                    'dev', interface_name])
        logger.info('Finished configuring network interface')
        return ep

    def _container_add(self, pid, interface):
        """
        Add a container (on this host) to Calico networking with the given IP.
        """
        # Check if the container already exists. If it does, exit.
        try:
            _ = self._datastore_client.get_endpoint(
                hostname=HOSTNAME,
                orchestrator_id=ORCHESTRATOR_ID,
                workload_id=self.docker_id
            )
        except KeyError:
            # Calico doesn't know about this container.  Continue.
            pass
        else:
            logger.error("This container has already been configured "
                         "with Calico Networking.")
            sys.exit(1)

        # Obtain information from Docker Client and validate container state
        self._validate_container_state(self.docker_id)

        ip_list = [self._assign_container_ip()]

        # Create Endpoint object
        try:
            logger.info("Creating endpoint with IPs %s", ip_list)
            ep = self._datastore_client.create_endpoint(HOSTNAME,
                                                        ORCHESTRATOR_ID,
                                                        self.docker_id,
                                                        ip_list)
        except (AddrFormatError, KeyError):
            logger.exception("Failed to create endpoint with IPs %s. "
                             "Unassigning IP address, then exiting.", ip_list)
            self._datastore_client.release_ips(set(ip_list))
            sys.exit(1)

        # Create the veth, move into the container namespace, add the IP and
        # set up the default routes.
        logger.debug("Creating the veth with namespace pid %s on interface "
                     "name %s", pid, interface)
        ep.mac = ep.provision_veth(netns.PidNamespace(pid), interface)

        logger.debug("Setting mac address %s to endpoint %s", ep.mac, ep.name)
        self._datastore_client.set_endpoint(ep)

        # Let the caller know what endpoint was created.
        return ep

    def _assign_container_ip(self):
        """
        Assign IPAddress either with the assigned docker IPAddress or utilize
        calico IPAM.

        True indicates to utilize Calico's auto_assign IPAM policy.
        False indicate to utilize the docker assigned IPAddress

        :return IPAddress which has been assigned
        """
        def _assign(ip):
            """
            Local helper function for assigning an IP and checking for errors.
            Only used when operating with CALICO_IPAM=false
            """
            try:
                logger.info("Attempting to assign IP %s", ip)
                self._datastore_client.assign_ip(ip, str(self.docker_id), None)
            except (ValueError, RuntimeError):
                logger.exception("Failed to assign IPAddress %s", ip)
                sys.exit(1)

        if self.calico_ipam == 'true':
            logger.info("Using Calico IPAM")
            try:
                ipv4s, ipv6s = self._datastore_client.auto_assign_ips(1, 0,
                                                        self.docker_id, None)
                ip = ipv4s[0]
                logger.debug("IPAM assigned ipv4=%s; ipv6= %s", ipv4s, ipv6s)
            except RuntimeError as err:
                logger.error("Cannot auto assign IPAddress: %s", err.message)
                sys.exit(1)
        else:
            logger.info("Using docker assigned IP address")
            ip = self._read_docker_ip()

            try:
                # Try to assign the address using the _assign helper function.
                _assign(ip)
            except AlreadyAssignedError:
                # If the Docker IP is already assigned, it is most likely that
                # an endpoint has been removed under our feet.  When using
                # Docker IPAM, treat Docker as the source of
                # truth for IP addresses.
                logger.warning("Docker IP is already assigned, finding "
                               "stale endpoint")
                self._datastore_client.release_ips(set([ip]))

                # Clean up whatever existing endpoint has this IP address.
                # We can improve this later by making use of IPAM attributes
                # in libcalico to store the endpoint ID.  For now,
                # just loop through endpoints on this host.
                endpoints = self._datastore_client.get_endpoints(
                    hostname=HOSTNAME,
                    orchestrator_id=ORCHESTRATOR_ID)
                for ep in endpoints:
                    if IPNetwork(ip) in ep.ipv4_nets:
                        logger.warning("Deleting stale endpoint %s",
                                       ep.endpoint_id)
                        for profile_id in ep.profile_ids:
                            self._datastore_client.remove_profile(profile_id)
                        self._datastore_client.remove_endpoint(ep)
                        break

                # Assign the IP address to the new endpoint.  It shouldn't
                # be assigned, since we just unassigned it.
                logger.warning("Retry Docker assigned IP")
                _assign(ip)
        return ip

    def _container_remove(self):
        """
        Remove the indicated container on this host from Calico networking
        """
        # Find the endpoint ID. We need this to find any ACL rules
        try:
            endpoint = self._datastore_client.get_endpoint(
                hostname=HOSTNAME,
                orchestrator_id=ORCHESTRATOR_ID,
                workload_id=self.docker_id
            )
        except KeyError:
            logger.exception("Container %s doesn't contain any endpoints",
                             self.docker_id)
            sys.exit(1)

        # Remove any IP address assignments that this endpoint has
        ip_set = set()
        for net in endpoint.ipv4_nets | endpoint.ipv6_nets:
            ip_set.add(net.ip)
        logger.info("Removing IP addresses %s from endpoint %s",
                    ip_set, endpoint.name)
        self._datastore_client.release_ips(ip_set)

        # Remove the veth interface from endpoint
        logger.info("Removing veth interfaces")
        try:
            netns.remove_veth(endpoint.name)
        except CalledProcessError:
            logger.exception("Could not remove veth interface from "
                             "endpoint %s", endpoint.name)

        # Remove the container/endpoint from the datastore.
        try:
            self._datastore_client.remove_workload(
                HOSTNAME, ORCHESTRATOR_ID, self.docker_id)
        except KeyError:
            logger.exception("Failed to remove workload.")
        logger.info("Removed Calico endpoint %s", endpoint.endpoint_id)

    def _validate_container_state(self, container_name):
        info = self._get_container_info(container_name)

        # Check the container is actually running.
        if not info["State"]["Running"]:
            logger.error("The container is not currently running.")
            sys.exit(1)

        # We can't set up Calico if the container shares the host namespace.
        if info["HostConfig"]["NetworkMode"] == "host":
            logger.warning("Calico cannot network container because "
                           "it is running NetworkMode = host.")
            sys.exit(0)

    def _uses_host_networking(self, container_name):
        """
        Returns true if the given container is running in the
        host network namespace.
        """
        info = self._get_container_info(container_name)
        return info["HostConfig"]["NetworkMode"] == "host"

    def _get_container_info(self, container_name):
        try:
            info = self._docker_client.inspect_container(container_name)
        except APIError as e:
            if e.response.status_code == 404:
                logger.error("Container %s was not found. Exiting.",
                             container_name)
            else:
                logger.error(e.message)
            sys.exit(1)
        return info

    def _get_container_pid(self, container_name):
        return self._get_container_info(container_name)["State"]["Pid"]

    def _read_docker_ip(self):
        """Get the IP for the pod's infra container."""
        container_info = self._get_container_info(self.docker_id)
        ip = container_info["NetworkSettings"]["IPAddress"]
        logger.info('Docker-assigned IP is %s', ip)
        return IPAddress(ip)

    def _get_node_ip(self):
        """
        Determine the IP for the host node.
        """
        # Compile list of addresses on network, return the first entry.
        # Try IPv4 and IPv6.
        addrs = get_host_ips(version=4) or get_host_ips(version=6)

        try:
            addr = addrs[0]
            logger.debug("Node's IP address: %s", addr)
            return addr
        except IndexError:
            # If both get_host_ips return empty lists, print message and exit.
            logger.exception('No Valid IP Address Found for Host - cannot '
                             'configure networking for pod %s. '
                             'Exiting', self.pod_name)
            sys.exit(1)

    def _delete_docker_interface(self):
        """Delete the existing veth connecting to the docker bridge."""
        logger.debug('Deleting docker interface eth0')

        # Get the PID of the container.
        pid = str(self._get_container_pid(self.docker_id))
        logger.debug('Container %s running with PID %s', self.docker_id, pid)

        # Set up a link to the container's netns.
        logger.debug("Linking to container's netns")
        logger.debug(check_output(['mkdir', '-p', '/var/run/netns']))
        netns_file = '/var/run/netns/' + pid
        if not os.path.isfile(netns_file):
            logger.debug(check_output(['ln', '-s', '/proc/' + pid + '/ns/net',
                                      netns_file]))

        # Log our container's interfaces before making any changes.
        _log_interfaces(pid)

        # Reach into the netns and delete the docker-allocated interface.
        logger.debug(check_output(['ip', 'netns', 'exec', pid,
                                  'ip', 'link', 'del', 'eth0']))

        # Log our container's interfaces after making our changes.
        _log_interfaces(pid)

        # Clean up after ourselves (don't want to leak netns files)
        logger.debug(check_output(['rm', netns_file]))

    def _get_pod_ports(self, pod):
        """
        Get the list of ports on containers in the Pod.

        :return list ports: the Kubernetes ContainerPort objects for the pod.
        """
        ports = []
        for container in pod['spec']['containers']:
            try:
                more_ports = container['ports']
                logger.info('Adding ports %s', more_ports)
                ports.extend(more_ports)
            except KeyError:
                pass
        return ports

    def _get_pod_config(self):
        """Get the pod resource from the API.
        API Path depends on the api_root, namespace, and pod_name

        :return: JSON object containing the pod spec
        """
        with requests.Session() as session:
            if self._api_root_secure() and self.auth_token:
                logger.debug('Updating header with Token %s', self.auth_token)
                session.headers.update({'Authorization':
                                        'Bearer ' + self.auth_token})

            path = os.path.join(self.api_root,
                                'namespaces/%s/pods/%s' % (self.namespace,
                                                           self.pod_name))
            try:
                logger.debug('Querying API for Pod: %s', path)
                response = session.get(path, verify=False)
            except BaseException:
                logger.exception("Exception hitting Kubernetes API")
                sys.exit(1)
            else:
                if response.status_code != 200:
                    logger.error("Response from API returned %s Error:\n%s",
                                 response.status_code,
                                 response.text)
                    sys.exit(response.status_code)

        logger.debug("API Response: %s", response.text)
        pod = json.loads(response.text)
        return pod

    def _api_root_secure(self):
        """
        Checks whether the Kubernetes api root is secure or insecure.
        If not an http or https address, exit.

        :return: Boolean: True if secure. False if insecure
        """
        if (self.api_root[:5] == 'https'):
            logger.debug('Using Secure API access.')
            return True
        elif (self.api_root[:5] == 'http:'):
            logger.debug('Using Insecure API access.')
            return False
        else:
            logger.error('%s is not set correctly (%s). '
                         'Please specify as http or https address. Exiting',
                         KUBE_API_ROOT_VAR, self.api_root)
            sys.exit(1)

    def _generate_rules(self, pod):
        """
        Generate Rules takes human readable policy strings in annotations
        and returns a libcalico Rules object.

        :return tuple of inbound_rules, outbound_rules
        """
        # Create allow and per-namespace rules for later use.
        allow = Rule(action="allow")
        allow_ns = Rule(action="allow", src_tag=self._get_namespace_tag(pod))
        annotations = self._get_metadata(pod, "annotations")
        logger.debug("Found annotations: %s", annotations)

        if self.namespace == "kube-system" :
            # Pods in the kube-system namespace must be accessible by all
            # other pods for services like DNS to work.
            logger.info("Pod is in kube-system namespace - allow all")
            inbound_rules = [allow]
            outbound_rules = [allow]
        elif annotations and POLICY_ANNOTATION_KEY in annotations:
            # If policy annotations are defined, use them to generate rules.
            logger.info("Generating advanced security policy from annotations")
            rules = annotations[POLICY_ANNOTATION_KEY]
            inbound_rules = []
            outbound_rules = [allow]
            for rule in rules.split(";"):
                parsed_rule = self.policy_parser.parse_line(rule)
                inbound_rules.append(parsed_rule)
        else:
            # If not annotations are defined, just use the configured
            # default policy.
            if self.default_policy == 'ns_isolation':
                # Isolate on namespace boundaries by default.
                logger.debug("Default policy is namespace isolation")
                inbound_rules = [allow_ns]
                outbound_rules = [allow]
            else:
                # Allow all traffic by default.
                logger.debug("Default policy is allow all")
                inbound_rules = [allow]
                outbound_rules = [allow]

        return Rules(id=self.profile_name,
                     inbound_rules=inbound_rules,
                     outbound_rules=outbound_rules)

    def _apply_tags(self, pod):
        """
        In addition to Calico's default pod_name tag,
        Add tags generated from Kubernetes Labels and Namespace
            Ex. labels: {key:value} -> tags+= namespace_key_value
        Add tag for namespace
            Ex. namespace: default -> tags+= namespace_default

        :param self.profile_name: The name of the Calico profile.
        :type self.profile_name: string
        :param pod: The config dictionary for the pod being created.
        :type pod: dict
        :return:
        """
        logger.debug('Applying tags')

        try:
            profile = self._datastore_client.get_profile(self.profile_name)
        except KeyError:
            logger.error('Could not apply tags. Profile %s could not be '
                         'found. Exiting', self.profile_name)
            sys.exit(1)

        # Grab namespace and create a tag if it exists.
        ns_tag = self._get_namespace_tag(pod)
        logger.debug('Adding tag %s', ns_tag)
        profile.tags.add(ns_tag)

        # Create tags from labels
        labels = self._get_metadata(pod, 'labels')
        if labels:
            for k, v in labels.iteritems():
                tag = self._label_to_tag(k, v)
                logger.debug('Adding tag %s', tag)
                profile.tags.add(tag)

        self._datastore_client.profile_update_tags(profile)
        logger.debug('Finished applying tags.')

    def _get_metadata(self, pod, key):
        """
        Return Metadata[key] Object given Pod
        Returns None if no key-value exists
        """
        try:
            val = pod['metadata'][key]
        except (KeyError, TypeError):
            logger.debug('No %s found in pod %s', key, pod)
            return None

        logger.debug("Pod %s: %s", key, val)
        return val

    def _escape_chars(self, unescaped_string):
        """
        Calico can only handle 3 special chars, '_.-'
        This function uses regex sub to replace SCs with '_'
        """
        # Character to replace symbols
        swap_char = '_'

        # If swap_char is in string, double it.
        unescaped_string = re.sub(swap_char, "%s%s" % (swap_char, swap_char),
                                  unescaped_string)

        # Substitute all invalid chars.
        return re.sub('[^a-zA-Z0-9\.\_\-]', swap_char, unescaped_string)

    def _get_namespace_tag(self, pod):
        """
        Pull metadata for namespace and return it and a generated NS tag
        """
        assert self.namespace
        ns_tag = self._escape_chars('%s=%s' % ('namespace', self.namespace))
        return ns_tag

    def _label_to_tag(self, label_key, label_value):
        """
        Labels are key-value pairs, tags are single strings. This function
        handles that translation.
        1) Concatenate key and value with '='
        2) Prepend a pod's namespace followed by '/' if available
        3) Escape the generated string so it is Calico compatible
        :param label_key: key to label
        :param label_value: value to given key for a label
        :param namespace: Namespace string, input None if not available
        :param types: (self, string, string, string)
        :return single string tag
        :rtype string
        """
        tag = '%s=%s' % (label_key, label_value)
        tag = '%s/%s' % (self.namespace, tag)
        tag = self._escape_chars(tag)
        return tag
class NetworkPlugin(object):

    def __init__(self, config):
        self.pod_name = None
        self.namespace = None
        self.docker_id = None
        self.policy_parser = None

        # Get configuration from the given dictionary.
        logger.debug("Plugin running with config: %s", config)
        self.auth_token = config[KUBE_AUTH_TOKEN_VAR]
        self.api_root = config[KUBE_API_ROOT_VAR]
        self.calico_ipam = config[CALICO_IPAM_VAR].lower()
        self.default_policy = config[DEFAULT_POLICY_VAR].lower()

        self._datastore_client = IPAMClient()
        self._docker_client = Client(
            version=DOCKER_VERSION,
            base_url=os.getenv("DOCKER_HOST", "unix://var/run/docker.sock"))

    def create(self, namespace, pod_name, docker_id):
        """"Create a pod."""
        self.pod_name = pod_name
        self.docker_id = docker_id
        self.namespace = namespace
        self.policy_parser = PolicyParser(self.namespace)
        logger.info('Configuring pod %s/%s (container_id %s)',
                    self.namespace, self.pod_name, self.docker_id)

        # Obtain information from Docker Client and validate container state.
        # If validation fails, the plugin will exit.
        self._validate_container_state(self.docker_id)

        try:
            endpoint = self._configure_interface()
            logger.info("Created Calico endpoint: %s", endpoint.endpoint_id)
            self._configure_profile(endpoint)
        except BaseException:
            # Check to see if an endpoint has been created.  If so,
            # we need to tear down any state we may have created.
            logger.exception("Error networking pod - cleaning up")

            try:
                self.delete(namespace, pod_name, docker_id)
            except BaseException:
                # Catch all errors tearing down the pod - this
                # is best-effort.
                logger.exception("Error cleaning up pod")

            # We've torn down, exit. 
            logger.info("Done cleaning up")
            sys.exit(1)
        else:
            logger.info("Successfully configured networking for pod %s/%s",
                        self.namespace, self.pod_name)

    def delete(self, namespace, pod_name, docker_id):
        """Cleanup after a pod."""
        self.pod_name = pod_name
        self.docker_id = docker_id
        self.namespace = namespace
        logger.info('Removing networking from pod %s/%s (container id %s)',
                    self.namespace, self.pod_name, self.docker_id)

        # Get the Calico endpoint.
        endpoint = self._get_endpoint()
        if not endpoint:
            # If there is no endpoint, we don't have any work to do - return.
            logger.debug("No Calico endpoint for pod, no work to do.")
            sys.exit(0)
        logger.debug("Pod has Calico endpoint %s", endpoint.endpoint_id)

        # Remove the endpoint and its configuration.
        self._remove_endpoint(endpoint)

        # Remove any profiles.
        self._remove_profiles(endpoint)

        logger.info("Successfully removed networking for pod %s/%s",
                    self.namespace, self.pod_name)

    def _remove_profiles(self, endpoint):
        """
        If the pod has any profiles, delete them unless they are the 
        default profile or have other members.  We can do this because 
        we create a profile per pod. Profile management for namespaces
        and service based policy will need to be done differently.
        """
        logger.debug("Endpoint has profiles: %s", endpoint.profile_ids)
        for profile_id in endpoint.profile_ids:
            if profile_id == DEFAULT_PROFILE_NAME:
                logger.debug("Do not delete default profile")
                continue

            if self._datastore_client.get_profile_members(profile_id):
                logger.info("Profile %s still has members, do not delete", 
                            profile_id)
                continue

            try:
                logger.info("Deleting Calico profile: %s", profile_id)
                self._datastore_client.remove_profile(profile_id)
            except KeyError:
                logger.warning("Cannot remove profile %s; Profile cannot "
                               "be found.", profile_id)


    def _get_endpoint(self):
        """
        Attempts to get and return the Calico endpoint for this pod.  If no
        endpoint exists, returns None.
        """
        logger.debug("Looking up endpoint for workload %s", self.docker_id)
        try:
            endpoint = self._datastore_client.get_endpoint(
                hostname=HOSTNAME,
                orchestrator_id=ORCHESTRATOR_ID,
                workload_id=self.docker_id
            )
        except KeyError:
            logger.debug("No Calico endpoint exists for pod %s/%s",
                         self.namespace, self.pod_name)
            endpoint = None
        return endpoint
 
    def status(self, namespace, pod_name, docker_id):
        self.namespace = namespace
        self.pod_name = pod_name
        self.docker_id = docker_id

        if self._uses_host_networking(self.docker_id):
            # We don't perform networking / assign IP addresses for pods running
            # in the host namespace, and so we can't return a status update
            # for them.
            logger.debug("Ignoring status for pod %s/%s in host namespace",
                         self.namespace, self.pod_name)
            sys.exit(0)

        # Get the endpoint.
        endpoint = self._get_endpoint()
        if not endpoint:
            # If the endpoint doesn't exist, we cannot provide a status.
            logger.debug("No endpoint for pod - cannot provide status")
            sys.exit(1)

        # Retrieve IPAddress from the attached IPNetworks on the endpoint
        # Since Kubernetes only supports ipv4, we'll only check for ipv4 nets
        if not endpoint.ipv4_nets:
            logger.error("Error in status: No IPs attached to pod %s/%s",
                         self.namespace, self.pod_name)
            sys.exit(1)
        else:
            ip_net = list(endpoint.ipv4_nets)
            if len(ip_net) is not 1:
                logger.warning("There is more than one IPNetwork attached "
                               "to pod %s/%s", self.namespace, self.pod_name)
            ip = ip_net[0].ip

        logger.debug("Retrieved pod IP Address: %s", ip)

        json_dict = {
            "apiVersion": "v1beta1",
            "kind": "PodNetworkStatus",
            "ip": str(ip)
        }

        logger.debug("Writing status to stdout: \n%s", json.dumps(json_dict))
        print(json.dumps(json_dict))

    def _configure_profile(self, endpoint):
        """
        Configure the calico profile on the given endpoint.

        If DEFAULT_POLICY != none, we create a new profile for this pod and populate it 
        with the correct rules.

        Otherwise, the pod gets assigned to the default profile.
        """
        if self.default_policy != POLICY_NONE:
            # Determine the name for this profile.
            profile_name = "%s_%s_%s" % (self.namespace, 
                                         self.pod_name, 
                                         str(self.docker_id)[:12])

            # Create a new profile for this pod.
            logger.info("Creating profile '%s'", profile_name)

            #  Retrieve pod labels, etc.
            pod = self._get_pod_config()
    
            if self._datastore_client.profile_exists(profile_name):
                # In profile-per-pod, we don't ever expect duplicate profiles.
                logger.error("Profile '%s' already exists.", profile_name)
                sys.exit(1)
            else:
                # The profile doesn't exist - generate the rule set for this 
                # profile, and create it.
                rules = self._generate_rules(pod, profile_name)
                self._datastore_client.create_profile(profile_name, rules)
    
            # Add tags to the profile based on labels.
            self._apply_tags(pod, profile_name)
    
            # Set the profile for the workload.
            logger.info("Setting profile '%s' on endpoint %s",
                        profile_name, endpoint.endpoint_id)
            self._datastore_client.set_profiles_on_endpoint(
                [profile_name], endpoint_id=endpoint.endpoint_id
            )
            logger.debug('Finished configuring profile.')
        else:
            # Policy is disabled - add this pod to the default profile.
            if not self._datastore_client.profile_exists(DEFAULT_PROFILE_NAME):
                # If the default profile doesn't exist, create it.
                logger.info("Creating profile '%s'", DEFAULT_PROFILE_NAME)
                allow = Rule(action="allow")
                rules = Rules(id=DEFAULT_PROFILE_NAME, 
                              inbound_rules=[allow], 
                              outbound_rules=[allow])
                self._datastore_client.create_profile(DEFAULT_PROFILE_NAME, 
                                                      rules)

            # Set the default profile on this pod's Calico endpoint.
            logger.info("Setting profile '%s' on endpoint %s", 
                        DEFAULT_PROFILE_NAME, endpoint.endpoint_id)
            self._datastore_client.set_profiles_on_endpoint(
                [DEFAULT_PROFILE_NAME], 
                endpoint_id=endpoint.endpoint_id
            )

    def _configure_interface(self):
        """Configure the Calico interface for a pod.

        This involves the following steps:
        1) Determine the IP that docker assigned to the interface inside the
           container
        2) Delete the docker-assigned veth pair that's attached to the docker
           bridge
        3) Create a new calico veth pair, using the docker-assigned IP for the
           end in the container's namespace
        4) Assign the node's IP to the host end of the veth pair (required for
           compatibility with kube-proxy REDIRECT iptables rules).
        """
        # Get container's PID.
        container_pid = self._get_container_pid(self.docker_id)

        self._delete_docker_interface()
        logger.info('Configuring Calico network interface')
        ep = self._create_endpoint(container_pid)

        # Log our container's interfaces after adding the new interface.
        _log_interfaces(container_pid)

        interface_name = generate_cali_interface_name(IF_PREFIX,
                                                      ep.endpoint_id)
        node_ip = self._get_node_ip()
        logger.debug('Adding node IP %s to host-side veth %s', node_ip, interface_name)

        # This is slightly tricky. Since the kube-proxy sometimes
        # programs REDIRECT iptables rules, we MUST have an IP on the host end
        # of the caliXXX veth pairs. This is because the REDIRECT rule
        # rewrites the destination ip/port of traffic from a pod to a service
        # VIP. The destination port is rewriten to an arbitrary high-numbered
        # port, and the destination IP is rewritten to one of the IPs allocated
        # to the interface. This fails if the interface doesn't have an IP,
        # so we allocate an IP which is already allocated to the node. We set
        # the subnet to /32 so that the routing table is not affected;
        # no traffic for the node_ip's subnet will use the /32 route.
        check_call(['ip', 'addr', 'add', node_ip + '/32',
                    'dev', interface_name])
        logger.info('Finished configuring network interface')
        return ep

    def _create_endpoint(self, pid):
        """
        Creates a Calico endpoint for this pod.
        - Assigns an IP address for this pod.
        - Creates the Calico endpoint object in the datastore.
        - Provisions the Calico veth pair for this pod.

        Returns the created libcalico Endpoint object.
        """
        # Check if the container already exists. If it does, exit.
        if self._get_endpoint():
            logger.error("This container has already been configured "
                         "with Calico Networking.")
            sys.exit(1)

        ip_list = [self._assign_container_ip()]

        # Create Endpoint object
        try:
            logger.info("Creating Calico endpoint with IPs %s", ip_list)
            ep = self._datastore_client.create_endpoint(HOSTNAME,
                                                        ORCHESTRATOR_ID,
                                                        self.docker_id,
                                                        ip_list)
        except (AddrFormatError, KeyError):
            # We failed to create the endpoint - we must release the IPs
            # that we assigned for this endpoint or else they will leak.
            logger.exception("Failed to create endpoint with IPs %s. "
                             "Unassigning IP address, then exiting.", ip_list)
            self._datastore_client.release_ips(set(ip_list))
            sys.exit(1)

        # Create the veth, move into the container namespace, add the IP and
        # set up the default routes.
        logger.debug("Creating eth0 in network namespace with pid=%s", pid) 
        ep.mac = ep.provision_veth(netns.PidNamespace(pid), "eth0")

        logger.debug("Setting mac address %s on endpoint %s", ep.mac, ep.name)
        self._datastore_client.set_endpoint(ep)

        # Let the caller know what endpoint was created.
        return ep

    def _assign_container_ip(self):
        """
        Assign IPAddress either with the assigned docker IPAddress or utilize
        calico IPAM.

        True indicates to utilize Calico's auto_assign IPAM policy.
        False indicate to utilize the docker assigned IPAddress

        :return IPAddress which has been assigned
        """
        def _assign(ip):
            """
            Local helper function for assigning an IP and checking for errors.
            Only used when operating with CALICO_IPAM=false
            """
            try:
                logger.info("Attempting to assign IP %s", ip)
                self._datastore_client.assign_ip(ip, str(self.docker_id), None)
            except (ValueError, RuntimeError):
                logger.exception("Failed to assign IPAddress %s", ip)
                sys.exit(1)

        if self.calico_ipam == 'true':
            logger.info("Using Calico IPAM")
            try:
                ipv4s, ipv6s = self._datastore_client.auto_assign_ips(1, 0,
                                                        self.docker_id, None)
                logger.debug("IPAM assigned ipv4=%s; ipv6= %s", ipv4s, ipv6s)
            except RuntimeError as err:
                logger.error("Cannot auto assign IP address: %s", err.message)
                sys.exit(1)

            # Check to make sure an address was assigned.
            if not ipv4s:
                logger.error("Unable to assign an IP address - exiting")
                sys.exit(1)

            # Get the address.
            ip = ipv4s[0]

        else:
            logger.info("Using docker assigned IP address")
            ip = self._read_docker_ip()

            try:
                # Try to assign the address using the _assign helper function.
                _assign(ip)
            except AlreadyAssignedError:
                # If the Docker IP is already assigned, it is most likely that
                # an endpoint has been removed under our feet.  When using
                # Docker IPAM, treat Docker as the source of
                # truth for IP addresses.
                logger.warning("Docker IP is already assigned, finding "
                               "stale endpoint")
                self._datastore_client.release_ips(set([ip]))

                # Clean up whatever existing endpoint has this IP address.
                # We can improve this later by making use of IPAM attributes
                # in libcalico to store the endpoint ID.  For now,
                # just loop through endpoints on this host.
                endpoints = self._datastore_client.get_endpoints(
                    hostname=HOSTNAME,
                    orchestrator_id=ORCHESTRATOR_ID)
                for ep in endpoints:
                    if IPNetwork(ip) in ep.ipv4_nets:
                        logger.warning("Deleting stale endpoint %s",
                                       ep.endpoint_id)
                        for profile_id in ep.profile_ids:
                            self._datastore_client.remove_profile(profile_id)
                        self._datastore_client.remove_endpoint(ep)
                        break

                # Assign the IP address to the new endpoint.  It shouldn't
                # be assigned, since we just unassigned it.
                logger.warning("Retry Docker assigned IP")
                _assign(ip)
        return ip

    def _remove_endpoint(self, endpoint):
        """
        Remove the provided endpoint on this host from Calico networking.
        - Removes any IP address assignments.
        - Removes the veth interface for this endpoint.
        - Removes the endpoint object from etcd.
        """
        # Remove any IP address assignments that this endpoint has
        ip_set = set()
        for net in endpoint.ipv4_nets | endpoint.ipv6_nets:
            ip_set.add(net.ip)
        logger.info("Removing IP addresses %s from endpoint %s",
                    ip_set, endpoint.name)
        self._datastore_client.release_ips(ip_set)

        # Remove the veth interface from endpoint
        logger.info("Removing veth interfaces")
        try:
            netns.remove_veth(endpoint.name)
        except CalledProcessError:
            logger.exception("Could not remove veth interface from "
                             "endpoint %s", endpoint.name)

        # Remove endpoint from the datastore.
        try:
            self._datastore_client.remove_workload(
                HOSTNAME, ORCHESTRATOR_ID, self.docker_id)
        except KeyError:
            logger.exception("Error removing workload.")
        logger.info("Removed Calico endpoint %s", endpoint.endpoint_id)

    def _validate_container_state(self, container_name):
        info = self._get_container_info(container_name)

        # Check the container is actually running.
        if not info["State"]["Running"]:
            logger.error("The infra container is not currently running.")
            sys.exit(1)

        # We can't set up Calico if the container shares the host namespace.
        if info["HostConfig"]["NetworkMode"] == "host":
            logger.info("Skipping pod %s/%s because "
                        "it is running NetworkMode = host.",
                        self.namespace, self.pod_name)
            sys.exit(0)

    def _uses_host_networking(self, container_name):
        """
        Returns true if the given container is running in the
        host network namespace.
        """
        info = self._get_container_info(container_name)
        return info["HostConfig"]["NetworkMode"] == "host"

    def _get_container_info(self, container_name):
        try:
            info = self._docker_client.inspect_container(container_name)
        except APIError as e:
            if e.response.status_code == 404:
                logger.error("Container %s was not found. Exiting.",
                             container_name)
            else:
                logger.error(e.message)
            sys.exit(1)
        return info

    def _get_container_pid(self, container_name):
        return self._get_container_info(container_name)["State"]["Pid"]

    def _read_docker_ip(self):
        """Get the IP for the pod's infra container."""
        container_info = self._get_container_info(self.docker_id)
        ip = container_info["NetworkSettings"]["IPAddress"]
        logger.info('Docker-assigned IP is %s', ip)
        return IPAddress(ip)

    def _get_node_ip(self):
        """
        Determine the IP for the host node.
        """
        # Compile list of addresses on network, return the first entry.
        # Try IPv4 and IPv6.
        addrs = get_host_ips(version=4) or get_host_ips(version=6)

        try:
            addr = addrs[0]
            logger.debug("Node's IP address: %s", addr)
            return addr
        except IndexError:
            # If both get_host_ips return empty lists, print message and exit.
            logger.exception('No Valid IP Address Found for Host - cannot '
                             'configure networking for pod %s. '
                             'Exiting', self.pod_name)
            sys.exit(1)

    def _delete_docker_interface(self):
        """Delete the existing veth connecting to the docker bridge."""
        logger.debug('Deleting docker interface eth0')

        # Get the PID of the container.
        pid = str(self._get_container_pid(self.docker_id))
        logger.debug('Container %s running with PID %s', self.docker_id, pid)

        # Set up a link to the container's netns.
        logger.debug("Linking to container's netns")
        logger.debug(check_output(['mkdir', '-p', '/var/run/netns']))
        netns_file = '/var/run/netns/' + pid
        if not os.path.isfile(netns_file):
            logger.debug(check_output(['ln', '-s', '/proc/' + pid + '/ns/net',
                                      netns_file]))

        # Log our container's interfaces before making any changes.
        _log_interfaces(pid)

        # Reach into the netns and delete the docker-allocated interface.
        logger.debug(check_output(['ip', 'netns', 'exec', pid,
                                  'ip', 'link', 'del', 'eth0']))

        # Log our container's interfaces after making our changes.
        _log_interfaces(pid)

        # Clean up after ourselves (don't want to leak netns files)
        logger.debug(check_output(['rm', netns_file]))

    def _get_pod_ports(self, pod):
        """
        Get the list of ports on containers in the Pod.

        :return list ports: the Kubernetes ContainerPort objects for the pod.
        """
        ports = []
        for container in pod['spec']['containers']:
            try:
                more_ports = container['ports']
                logger.info('Adding ports %s', more_ports)
                ports.extend(more_ports)
            except KeyError:
                pass
        return ports

    def _get_pod_config(self):
        """Get the pod resource from the API.
        API Path depends on the api_root, namespace, and pod_name

        :return: JSON object containing the pod spec
        """
        with requests.Session() as session:
            if self._api_root_secure() and self.auth_token:
                logger.debug('Updating header with Token %s', self.auth_token)
                session.headers.update({'Authorization':
                                        'Bearer ' + self.auth_token})

            path = os.path.join(self.api_root,
                                'namespaces/%s/pods/%s' % (self.namespace,
                                                           self.pod_name))
            try:
                logger.debug('Querying API for Pod: %s', path)
                response = session.get(path, verify=False)
            except BaseException:
                logger.exception("Exception hitting Kubernetes API")
                sys.exit(1)
            else:
                if response.status_code != 200:
                    logger.error("Response from API returned %s Error:\n%s",
                                 response.status_code,
                                 response.text)
                    sys.exit(response.status_code)

        logger.debug("API Response: %s", response.text)
        pod = json.loads(response.text)
        return pod

    def _api_root_secure(self):
        """
        Checks whether the Kubernetes api root is secure or insecure.
        If not an http or https address, exit.

        :return: Boolean: True if secure. False if insecure
        """
        if (self.api_root[:5] == 'https'):
            logger.debug('Using Secure API access.')
            return True
        elif (self.api_root[:5] == 'http:'):
            logger.debug('Using Insecure API access.')
            return False
        else:
            logger.error('%s is not set correctly (%s). '
                         'Please specify as http or https address. Exiting',
                         KUBE_API_ROOT_VAR, self.api_root)
            sys.exit(1)

    def _generate_rules(self, pod, profile_name):
        """
        Generate Rules takes human readable policy strings in annotations
        and returns a libcalico Rules object.

        :return Pycalico Rules object. 
        """
        # Create allow and per-namespace rules for later use.
        allow = Rule(action="allow")
        allow_ns = Rule(action="allow", src_tag=self._get_namespace_tag(pod))
        annotations = self._get_metadata(pod, "annotations")
        logger.debug("Found annotations: %s", annotations)

        if self.namespace == "kube-system" :
            # Pods in the kube-system namespace must be accessible by all
            # other pods for services like DNS to work.
            logger.info("Pod is in kube-system namespace - allow all")
            inbound_rules = [allow]
            outbound_rules = [allow]
        elif annotations and POLICY_ANNOTATION_KEY in annotations:
            # If policy annotations are defined, use them to generate rules.
            logger.info("Generating advanced security policy from annotations")
            rules = annotations[POLICY_ANNOTATION_KEY]
            inbound_rules = []
            outbound_rules = [allow]
            for rule in rules.split(";"):
                parsed_rule = self.policy_parser.parse_line(rule)
                inbound_rules.append(parsed_rule)
        else:
            # If not annotations are defined, just use the configured
            # default policy.
            if self.default_policy == POLICY_NS_ISOLATION:
                # Isolate on namespace boundaries by default.
                logger.debug("Default policy is namespace isolation")
                inbound_rules = [allow_ns]
                outbound_rules = [allow]
            elif self.default_policy == POLICY_ALLOW:
                # Allow all traffic by default.
                logger.debug("Default policy is allow all")
                inbound_rules = [allow]
                outbound_rules = [allow]

        return Rules(id=profile_name,
                     inbound_rules=inbound_rules,
                     outbound_rules=outbound_rules)

    def _apply_tags(self, pod, profile_name):
        """
        In addition to Calico's default pod_name tag,
        Add tags generated from Kubernetes Labels and Namespace
            Ex. labels: {key:value} -> tags+= namespace_key_value
        Add tag for namespace
            Ex. namespace: default -> tags+= namespace_default

        :param profile_name: The name of the Calico profile.
        :type profile_name: string
        :param pod: The config dictionary for the pod being created.
        :type pod: dict
        :return:
        """
        logger.debug("Applying tags to profile '%s'", profile_name)

        try:
            profile = self._datastore_client.get_profile(profile_name)
        except KeyError:
            logger.error('Could not apply tags. Profile %s could not be '
                         'found. Exiting', profile_name)
            sys.exit(1)

        # Grab namespace and create a tag if it exists.
        ns_tag = self._get_namespace_tag(pod)
        logger.debug('Generated tag: %s', ns_tag)
        profile.tags.add(ns_tag)

        # Create tags from labels
        labels = self._get_metadata(pod, 'labels')
        if labels:
            for k, v in labels.iteritems():
                tag = self._label_to_tag(k, v)
                logger.debug('Generated tag: %s', tag)
                profile.tags.add(tag)

        # Apply tags to profile.
        self._datastore_client.profile_update_tags(profile)
        logger.debug('Finished applying tags.')

    def _get_metadata(self, pod, key):
        """
        Return Metadata[key] Object given Pod
        Returns None if no key-value exists
        """
        try:
            val = pod['metadata'][key]
        except (KeyError, TypeError):
            logger.debug('No %s found in pod %s', key, pod)
            return None

        logger.debug("Pod %s: %s", key, val)
        return val

    def _escape_chars(self, unescaped_string):
        """
        Calico can only handle 3 special chars, '_.-'
        This function uses regex sub to replace SCs with '_'
        """
        # Character to replace symbols
        swap_char = '_'

        # If swap_char is in string, double it.
        unescaped_string = re.sub(swap_char, "%s%s" % (swap_char, swap_char),
                                  unescaped_string)

        # Substitute all invalid chars.
        return re.sub('[^a-zA-Z0-9\.\_\-]', swap_char, unescaped_string)

    def _get_namespace_tag(self, pod):
        """
        Pull metadata for namespace and return it and a generated NS tag
        """
        assert self.namespace
        ns_tag = self._escape_chars('%s=%s' % ('namespace', self.namespace))
        return ns_tag

    def _label_to_tag(self, label_key, label_value):
        """
        Labels are key-value pairs, tags are single strings. This function
        handles that translation.
        1) Concatenate key and value with '='
        2) Prepend a pod's namespace followed by '/' if available
        3) Escape the generated string so it is Calico compatible
        :param label_key: key to label
        :param label_value: value to given key for a label
        :param namespace: Namespace string, input None if not available
        :param types: (self, string, string, string)
        :return single string tag
        :rtype string
        """
        tag = '%s=%s' % (label_key, label_value)
        tag = '%s/%s' % (self.namespace, tag)
        tag = self._escape_chars(tag)
        return tag
Exemple #13
0
 def __init__(self, 
              uriKeys=defaults.uriKeys,
              directiveKeys=defaults.directiveKeys,
              policyKeys=defaults.policyKeys,
              keyNameReplacements=defaults.reportKeyNameReplacements,
              requiredKeys=defaults.requiredReportKeys,
              strict=True,
              addSchemeToURIs=False,
              defaultURIScheme=defaults.defaultURIScheme,
              addPortToURIs=False,
              defaultURIPort=defaults.defaultURIPort,
              schemePortMappings=defaults.schemePortMappings,
              portSchemeMappings=defaults.portSchemeMappings,
              directiveTypeTranslations=defaults.directiveTypeTranslations, 
              allowedDirectiveTypes=defaults.allowedDirectiveTypes,
              ignoredDirectiveTypes=defaults.ignoredDirectiveTypes,
              expandDefaultSrc=False,
              defaultSrcTypes=defaults.defaultSrcReplacementDirectiveTypes):
     """
     Creates a new ReportParser object configured with the following parameters:
     
     'uriKeys': an iterable of key (entry) names of which the corresponding values, if present,
                         will be parsed and replaced with an URI object.
     'directiveKeys': an iterable of key (entry) names of which the corresponding values, if present,
                         will be parsed and replaced with a Directive object.
     'policyKeys': an iterable of key (entry) names of which the corresponding values, if present,
                         will be parsed and replaced with a Policy object.
     The 'uriKeys', 'directiveKeys', and 'policyKeys' lists are mutually exclusive (a key may appear
                         in at most one of these lists.)
                         
     'keyNameReplacements': a dictionary of old key (entry) names mapped to the new name to be given
                         to them before any further parsing. This can be used to adjust to renamed 
                         fields in the violation reports generated by different browser versions.
     'requiredKeys': an iterable of key (entry) names that are mandatory and must appear in the report
                         with a valid value (if parsed, cannot be URI.INVALID()/Directive.INVALID()/
                         Policy.INVALID()). If this constraint is violated, the parsing result will be
                         Report.INVALID() (independent of the 'strict' setting). This restriction is 
                         applied after performing key name replacement.
     'strict': whether a parsing error of a child element should be ignored if it can be fixed (if
                         set to False, invalid children will be skipped), or if any parsing error
                         should cause the Report to become Report.INVALID() (if set to True).
     
     'addSchemeToURIs': [for parsed URIs] whether to add the scheme. (See URIParser for details.)
     'defaultURIScheme': [for parsed URIs] if the scheme should be added, the default scheme to be 
                         assumed if nothing can be inferred from the port. (See URIParser for details.)
     'addPortToURIs': [for parsed URIs] whether to add the port. (See URIParser for details.)
     'defaultURIPort': [for parsed URIs] if the port should be added, the default port to be assumed
                         if nothing can be inferred from the scheme. (See URIParser for details.)
     'schemePortMappings': [for parsed URIs and policy/directive parsing] A map from scheme names to the 
                         corresponding default port, or None if the scheme does not use ports. Any scheme
                         that may appear inside an URI, source expression, directive or policy should be
                         listed. (See URIParser and SourceExpressionParser for details.)
     'portSchemeMappings': [for parsed URIs] A map from port numbers to scheme names (only for "real" ports).
                         See URIParser for details.
     'directiveTypeTranslations': [for parsed directives and policies] A map from the old directive name to
                         the new name to be used. (See DirectiveParser for details.)
     'allowedDirectiveTypes': [for parsed directives and policies] a list of directive types that are allowed.
                         (See DirectiveParser or PolicyParser for details.)
     'ignoredDirectiveTypes': [for parsed policies] a list of directive types that are ignored when parsing
                         policies. (See PolicyParser for details.)
     'expandDefaultSrc': [for parsed policies] if set to True, each "default-src" directive in a parsed policy
                         will be expanded to the corresponding elementary directive types, if not yet present.
                         (See PolicyParser for details.)
     'defaultSrcTypes': [for parsed policies] when "default-src" is expanded, the elementary directive types
                         that will be added to replace the default policy. (See PolicyParser for details.)
     """
     self._strict = strict
     self._uriKeys = uriKeys
     self._directiveKeys = directiveKeys
     self._policyKeys = policyKeys
     self._keyNameReplacements = keyNameReplacements
     self._requiredKeys = requiredKeys
     
     self._uriParser = URIParser(addSchemeToURIs, defaultURIScheme, addPortToURIs, defaultURIPort,
                                 schemePortMappings, portSchemeMappings, True)
     self._directiveParser = DirectiveParser(directiveTypeTranslations, allowedDirectiveTypes, 
                                 schemePortMappings.keys(), strict)
     self._policyParser = PolicyParser(directiveTypeTranslations, allowedDirectiveTypes, ignoredDirectiveTypes, 
                                 schemePortMappings.keys(), strict, expandDefaultSrc, defaultSrcTypes)
Exemple #14
0
class ReportParser(object):
    """
    Pre-configured object that parses strings or JSON dictionaries into Reports.
    """
    
    def __init__(self, 
                 uriKeys=defaults.uriKeys,
                 directiveKeys=defaults.directiveKeys,
                 policyKeys=defaults.policyKeys,
                 keyNameReplacements=defaults.reportKeyNameReplacements,
                 requiredKeys=defaults.requiredReportKeys,
                 strict=True,
                 addSchemeToURIs=False,
                 defaultURIScheme=defaults.defaultURIScheme,
                 addPortToURIs=False,
                 defaultURIPort=defaults.defaultURIPort,
                 schemePortMappings=defaults.schemePortMappings,
                 portSchemeMappings=defaults.portSchemeMappings,
                 directiveTypeTranslations=defaults.directiveTypeTranslations, 
                 allowedDirectiveTypes=defaults.allowedDirectiveTypes,
                 ignoredDirectiveTypes=defaults.ignoredDirectiveTypes,
                 expandDefaultSrc=False,
                 defaultSrcTypes=defaults.defaultSrcReplacementDirectiveTypes):
        """
        Creates a new ReportParser object configured with the following parameters:
        
        'uriKeys': an iterable of key (entry) names of which the corresponding values, if present,
                            will be parsed and replaced with an URI object.
        'directiveKeys': an iterable of key (entry) names of which the corresponding values, if present,
                            will be parsed and replaced with a Directive object.
        'policyKeys': an iterable of key (entry) names of which the corresponding values, if present,
                            will be parsed and replaced with a Policy object.
        The 'uriKeys', 'directiveKeys', and 'policyKeys' lists are mutually exclusive (a key may appear
                            in at most one of these lists.)
                            
        'keyNameReplacements': a dictionary of old key (entry) names mapped to the new name to be given
                            to them before any further parsing. This can be used to adjust to renamed 
                            fields in the violation reports generated by different browser versions.
        'requiredKeys': an iterable of key (entry) names that are mandatory and must appear in the report
                            with a valid value (if parsed, cannot be URI.INVALID()/Directive.INVALID()/
                            Policy.INVALID()). If this constraint is violated, the parsing result will be
                            Report.INVALID() (independent of the 'strict' setting). This restriction is 
                            applied after performing key name replacement.
        'strict': whether a parsing error of a child element should be ignored if it can be fixed (if
                            set to False, invalid children will be skipped), or if any parsing error
                            should cause the Report to become Report.INVALID() (if set to True).
        
        'addSchemeToURIs': [for parsed URIs] whether to add the scheme. (See URIParser for details.)
        'defaultURIScheme': [for parsed URIs] if the scheme should be added, the default scheme to be 
                            assumed if nothing can be inferred from the port. (See URIParser for details.)
        'addPortToURIs': [for parsed URIs] whether to add the port. (See URIParser for details.)
        'defaultURIPort': [for parsed URIs] if the port should be added, the default port to be assumed
                            if nothing can be inferred from the scheme. (See URIParser for details.)
        'schemePortMappings': [for parsed URIs and policy/directive parsing] A map from scheme names to the 
                            corresponding default port, or None if the scheme does not use ports. Any scheme
                            that may appear inside an URI, source expression, directive or policy should be
                            listed. (See URIParser and SourceExpressionParser for details.)
        'portSchemeMappings': [for parsed URIs] A map from port numbers to scheme names (only for "real" ports).
                            See URIParser for details.
        'directiveTypeTranslations': [for parsed directives and policies] A map from the old directive name to
                            the new name to be used. (See DirectiveParser for details.)
        'allowedDirectiveTypes': [for parsed directives and policies] a list of directive types that are allowed.
                            (See DirectiveParser or PolicyParser for details.)
        'ignoredDirectiveTypes': [for parsed policies] a list of directive types that are ignored when parsing
                            policies. (See PolicyParser for details.)
        'expandDefaultSrc': [for parsed policies] if set to True, each "default-src" directive in a parsed policy
                            will be expanded to the corresponding elementary directive types, if not yet present.
                            (See PolicyParser for details.)
        'defaultSrcTypes': [for parsed policies] when "default-src" is expanded, the elementary directive types
                            that will be added to replace the default policy. (See PolicyParser for details.)
        """
        self._strict = strict
        self._uriKeys = uriKeys
        self._directiveKeys = directiveKeys
        self._policyKeys = policyKeys
        self._keyNameReplacements = keyNameReplacements
        self._requiredKeys = requiredKeys
        
        self._uriParser = URIParser(addSchemeToURIs, defaultURIScheme, addPortToURIs, defaultURIPort,
                                    schemePortMappings, portSchemeMappings, True)
        self._directiveParser = DirectiveParser(directiveTypeTranslations, allowedDirectiveTypes, 
                                    schemePortMappings.keys(), strict)
        self._policyParser = PolicyParser(directiveTypeTranslations, allowedDirectiveTypes, ignoredDirectiveTypes, 
                                    schemePortMappings.keys(), strict, expandDefaultSrc, defaultSrcTypes)
    
    def parseString(self, stringReport):
        """
        Parses the given 'stringReport' according to the parameters set in the constructor of this ReportParser 
        and returns a Report object. 'stringReport' is expected to be a JSON-serialised map with attribute names
        and values corresponding to the definition of CSP violation reports. If 'stringReport' cannot be parsed 
        because it is syntactically invalid (or empty), Report.INVALID() will be returned.

        Depending on the configuration of this ReportParser object, some attributes will be parsed to replace their
        plain string values with a more high-level object representation.
        """
        try:
            jsonDict = json.loads(stringReport)
            return self.parseJsonDict(jsonDict)
        except ValueError:
            return Report.INVALID()
    
    def parseJsonDict(self, jsonReport):
        """
        Parses the given 'jsonReport' according to the parameters set in the constructor of this ReportParser 
        and returns a Report object. 'jsonReport' is expected to be a Python dict object with attribute names
        and values corresponding to the definition of CSP violation reports. If 'jsonReport' cannot be parsed 
        because it is syntactically invalid (or empty), Report.INVALID() will be returned.

        Depending on the configuration of this ReportParser object, some attributes will be parsed to replace their
        plain string values with a more high-level object representation.
        """
        
        # replace names
        renamedReport = dict(map(lambda (key, val): (self._replaceName(key), val), jsonReport.iteritems()))
                
        # convert data in report
        convertedReport = {}
        deferredSelfURIs = set([]) # all key names that have URIs that are exactly 'self' (handle after parsing everything else)
        for (key, value) in renamedReport.iteritems():
            if key in self._uriKeys:
                if value.lower().strip() == "self":
                    deferredSelfURIs.add(key)
                    continue
                else:
                    value = self._uriParser.parse(value)
            elif key in self._directiveKeys:
                value = self._directiveParser.parse(value)
            elif key in self._policyKeys:
                value = self._policyParser.parse(value)
            
            if value in (URI.INVALID(), Directive.INVALID(), Policy.INVALID()):
                if self._strict:
                    return Report.INVALID()
                else:
                    continue
            convertedReport[key] = value
            
        # handle deferred parsing of 'self' URIs (they represent the document-uri)
        for key in deferredSelfURIs:
            if "document-uri" in self._uriKeys and "document-uri" in convertedReport:
                convertedReport[key] = convertedReport["document-uri"]
            elif self._strict:
                return Report.INVALID()
            
        for requiredKey in self._requiredKeys:
            if not requiredKey in convertedReport:
                return Report.INVALID()
        return Report(convertedReport)

    def _replaceName(self, oldName):
        oldName = oldName.lower()
        if oldName in self._keyNameReplacements:
            return self._keyNameReplacements[oldName]
        else:
            return oldName