| |
| # Copyright 2017-present Open Networking Foundation |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """ |
| sync_service.py |
| |
| Synchronize Services. The only type of Service this step knows how to deal with are services that use Kubernetes |
| NodePort to expose ports. |
| """ |
| |
| from synchronizers.new_base.syncstep import SyncStep |
| from synchronizers.new_base.modelaccessor import Service |
| |
| from xosconfig import Config |
| from multistructlog import create_logger |
| |
| log = create_logger(Config().get('logging')) |
| |
| class SyncService(SyncStep): |
| |
| """ |
| SyncService |
| |
| Implements sync step for syncing Services. |
| """ |
| |
| provides = [Service] |
| observes = Service |
| requested_interval = 0 |
| |
| def __init__(self, *args, **kwargs): |
| super(SyncService, self).__init__(*args, **kwargs) |
| self.init_kubernetes_client() |
| |
| def init_kubernetes_client(self): |
| from kubernetes.client.rest import ApiException |
| from kubernetes import client as kubernetes_client, config as kubernetes_config |
| kubernetes_config.load_incluster_config() |
| self.kubernetes_client = kubernetes_client |
| self.v1core = kubernetes_client.CoreV1Api() |
| self.ApiException = ApiException |
| |
| def fetch_pending(self, deletion=False): |
| """ Filter the set of pending objects. |
| As this syncstep can only create Service that exist within Trust Domains, filter out those services that |
| don't have Trust Domains associated with them. |
| """ |
| models = super(SyncService, self).fetch_pending(deletion) |
| |
| if (not deletion): |
| for model in models[:]: |
| if not self.get_trust_domain(model): |
| # If this happens, then either the Service has no Slices, or it does have slices but none of |
| # those slices are associated with a TrustDomain. Assume the developer has done this on purpose |
| # and ignore the Service. |
| log.debug("Unable to determine Trust Domain for service %s. Ignoring." % model.name) |
| models.remove(model) |
| elif not model.serviceports.exists(): |
| # If there are not ServicePorts, then there's not much for us to do at this time... |
| log.debug("Service %s has no serviceports. Ignoring." % model.name) |
| models.remove(model) |
| |
| return models |
| |
| def get_trust_domain(self, o): |
| """ Given a service, determine its Trust Domain. |
| |
| The design we've chosen to go with is that a service is pinned to a Trust Domain based on the slices |
| that it contains. It's an error for a service to be directly comprised of slices from multiple |
| trust domains. |
| |
| This allows for "logical services", that contain no slices of their own, but are comprised of multiple |
| subservices. For example, EPC. |
| """ |
| |
| trust_domain = None |
| for slice in o.slices.all(): |
| if slice.trust_domain: |
| if (trust_domain is None): |
| trust_domain = slice.trust_domain |
| elif (trust_domain.id != slice.trust_domain.id): |
| # Bail out of we've encountered a situation where a service spans multiple trust domains. |
| log.warning("Service %s is comprised of slices from multiple trust domains." % o.name) |
| return None |
| |
| return trust_domain |
| |
| def get_service(self, o, trust_domain_name): |
| """ Given an XOS Service, read the associated Service from Kubernetes. |
| If no Kubernetes service exists, return None |
| """ |
| try: |
| k8s_service = self.v1core.read_namespaced_service(o.name, trust_domain_name) |
| except self.ApiException, e: |
| if e.status == 404: |
| return None |
| raise |
| return k8s_service |
| |
| def sync_record(self, o): |
| trust_domain = self.get_trust_domain(o) |
| k8s_service = self.get_service(o,trust_domain.name) |
| |
| if not k8s_service: |
| k8s_service = self.kubernetes_client.V1Service() |
| k8s_service.metadata = self.kubernetes_client.V1ObjectMeta(name=o.name) |
| |
| ports=[] |
| for service_port in o.serviceports.all(): |
| port=self.kubernetes_client.V1ServicePort(name = service_port.name, |
| node_port = service_port.external_port, |
| port = service_port.internal_port, |
| target_port = service_port.internal_port, |
| protocol = service_port.protocol) |
| ports.append(port) |
| |
| k8s_service.spec = self.kubernetes_client.V1ServiceSpec(ports=ports, |
| type="NodePort") |
| |
| k8s_service = self.v1core.create_namespaced_service(trust_domain.name, k8s_service) |
| |
| if (not o.backend_handle): |
| o.backend_handle = k8s_service.metadata.self_link |
| o.save(update_fields=["backend_handle"]) |
| |
| def delete_record(self, o): |
| trust_domain_name = None |
| trust_domain = self.get_trust_domain(o) |
| if trust_domain: |
| trust_domain_name = trust_domain.name |
| else: |
| # rely on backend_handle being structured like this one, |
| # /api/v1/namespaces/service1-trust/services/service1 |
| if (o.backend_handle): |
| parts = o.backend_handle.split("/") |
| if len(parts)>3: |
| trust_domain_name = parts[-3] |
| |
| if not trust_domain_name: |
| raise Exception("Can't delete service %s because there is no trust domain" % o.name) |
| |
| k8s_service = self.get_service(o, trust_domain_name) |
| if not k8s_service: |
| log.info("Kubernetes service does not exist; Nothing to delete.", o=o) |
| return |
| delete_options = self.kubernetes_client.V1DeleteOptions() |
| self.v1core.delete_namespaced_service(o.name, trust_domain_name, delete_options) |
| log.info("Deleted service from kubernetes", handle=o.backend_handle) |