blob: 230a0f4196ba7dc0579e83be93faef0d49c98e8a [file] [log] [blame]
Scott Baker3fd18e52018-04-17 09:18:21 -07001
2# Copyright 2017-present Open Networking Foundation
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16"""
17 sync_service.py
18
19 Synchronize Services. The only type of Service this step knows how to deal with are services that use Kubernetes
20 NodePort to expose ports.
21"""
22
23from synchronizers.new_base.syncstep import SyncStep
24from synchronizers.new_base.modelaccessor import Service
25
26from xosconfig import Config
27from multistructlog import create_logger
28
Scott Baker3fd18e52018-04-17 09:18:21 -070029log = create_logger(Config().get('logging'))
30
31class SyncService(SyncStep):
32
33 """
34 SyncService
35
36 Implements sync step for syncing Services.
37 """
38
39 provides = [Service]
40 observes = Service
41 requested_interval = 0
42
43 def __init__(self, *args, **kwargs):
44 super(SyncService, self).__init__(*args, **kwargs)
Scott Baker13e953c2018-05-17 09:19:15 -070045 self.init_kubernetes_client()
46
47 def init_kubernetes_client(self):
48 from kubernetes.client.rest import ApiException
49 from kubernetes import client as kubernetes_client, config as kubernetes_config
Scott Baker3fd18e52018-04-17 09:18:21 -070050 kubernetes_config.load_incluster_config()
Scott Baker13e953c2018-05-17 09:19:15 -070051 self.kubernetes_client = kubernetes_client
52 self.v1core = kubernetes_client.CoreV1Api()
53 self.ApiException = ApiException
Scott Baker3fd18e52018-04-17 09:18:21 -070054
55 def fetch_pending(self, deletion=False):
56 """ Filter the set of pending objects.
57 As this syncstep can only create Service that exist within Trust Domains, filter out those services that
58 don't have Trust Domains associated with them.
59 """
60 models = super(SyncService, self).fetch_pending(deletion)
61
62 if (not deletion):
63 for model in models[:]:
64 if not self.get_trust_domain(model):
Scott Baker04dd3e62018-11-27 17:17:13 -080065 # If this happens, then either the Service has no Slices, or it does have slices but none of
66 # those slices are associated with a TrustDomain. Assume the developer has done this on purpose
67 # and ignore the Service.
68 log.debug("Unable to determine Trust Domain for service %s. Ignoring." % model.name)
Scott Baker3fd18e52018-04-17 09:18:21 -070069 models.remove(model)
70 elif not model.serviceports.exists():
71 # If there are not ServicePorts, then there's not much for us to do at this time...
Scott Baker04dd3e62018-11-27 17:17:13 -080072 log.debug("Service %s has no serviceports. Ignoring." % model.name)
Scott Baker3fd18e52018-04-17 09:18:21 -070073 models.remove(model)
74
75 return models
76
77 def get_trust_domain(self, o):
78 """ Given a service, determine its Trust Domain.
79
80 The design we've chosen to go with is that a service is pinned to a Trust Domain based on the slices
81 that it contains. It's an error for a service to be directly comprised of slices from multiple
82 trust domains.
83
84 This allows for "logical services", that contain no slices of their own, but are comprised of multiple
85 subservices. For example, EPC.
86 """
87
88 trust_domain = None
89 for slice in o.slices.all():
90 if slice.trust_domain:
91 if (trust_domain is None):
92 trust_domain = slice.trust_domain
93 elif (trust_domain.id != slice.trust_domain.id):
94 # Bail out of we've encountered a situation where a service spans multiple trust domains.
95 log.warning("Service %s is comprised of slices from multiple trust domains." % o.name)
96 return None
97
98 return trust_domain
99
Scott Baker393d0152018-05-21 09:17:49 -0700100 def get_service(self, o, trust_domain_name):
Scott Baker3fd18e52018-04-17 09:18:21 -0700101 """ Given an XOS Service, read the associated Service from Kubernetes.
102 If no Kubernetes service exists, return None
103 """
104 try:
Scott Baker393d0152018-05-21 09:17:49 -0700105 k8s_service = self.v1core.read_namespaced_service(o.name, trust_domain_name)
Scott Baker13e953c2018-05-17 09:19:15 -0700106 except self.ApiException, e:
Scott Baker3fd18e52018-04-17 09:18:21 -0700107 if e.status == 404:
108 return None
109 raise
110 return k8s_service
111
112 def sync_record(self, o):
113 trust_domain = self.get_trust_domain(o)
Scott Baker393d0152018-05-21 09:17:49 -0700114 k8s_service = self.get_service(o,trust_domain.name)
Scott Baker3fd18e52018-04-17 09:18:21 -0700115
116 if not k8s_service:
Scott Baker13e953c2018-05-17 09:19:15 -0700117 k8s_service = self.kubernetes_client.V1Service()
118 k8s_service.metadata = self.kubernetes_client.V1ObjectMeta(name=o.name)
Scott Baker3fd18e52018-04-17 09:18:21 -0700119
120 ports=[]
121 for service_port in o.serviceports.all():
Scott Baker13e953c2018-05-17 09:19:15 -0700122 port=self.kubernetes_client.V1ServicePort(name = service_port.name,
Scott Baker3fd18e52018-04-17 09:18:21 -0700123 node_port = service_port.external_port,
124 port = service_port.internal_port,
125 target_port = service_port.internal_port,
126 protocol = service_port.protocol)
127 ports.append(port)
128
Scott Baker13e953c2018-05-17 09:19:15 -0700129 k8s_service.spec = self.kubernetes_client.V1ServiceSpec(ports=ports,
Scott Baker3fd18e52018-04-17 09:18:21 -0700130 type="NodePort")
131
Scott Baker13e953c2018-05-17 09:19:15 -0700132 k8s_service = self.v1core.create_namespaced_service(trust_domain.name, k8s_service)
133
134 if (not o.backend_handle):
135 o.backend_handle = k8s_service.metadata.self_link
136 o.save(update_fields=["backend_handle"])
Scott Baker3fd18e52018-04-17 09:18:21 -0700137
138 def delete_record(self, o):
Scott Baker393d0152018-05-21 09:17:49 -0700139 trust_domain_name = None
140 trust_domain = self.get_trust_domain(o)
141 if trust_domain:
142 trust_domain_name = trust_domain.name
143 else:
144 # rely on backend_handle being structured like this one,
145 # /api/v1/namespaces/service1-trust/services/service1
146 if (o.backend_handle):
147 parts = o.backend_handle.split("/")
148 if len(parts)>3:
149 trust_domain_name = parts[-3]
Scott Baker3fd18e52018-04-17 09:18:21 -0700150
Scott Baker393d0152018-05-21 09:17:49 -0700151 if not trust_domain_name:
152 raise Exception("Can't delete service %s because there is no trust domain" % o.name)
153
154 k8s_service = self.get_service(o, trust_domain_name)
155 if not k8s_service:
156 log.info("Kubernetes service does not exist; Nothing to delete.", o=o)
157 return
158 delete_options = self.kubernetes_client.V1DeleteOptions()
159 self.v1core.delete_namespaced_service(o.name, trust_domain_name, delete_options)
160 log.info("Deleted service from kubernetes", handle=o.backend_handle)