blob: 1aa92668c5c1c60b6f9ef6d28ae9608f96f6e885 [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
Scott Bakera30fae72019-02-01 16:14:43 -080023from xossynchronizer.steps.syncstep import SyncStep
24from xossynchronizer.modelaccessor import Service
Scott Baker3fd18e52018-04-17 09:18:21 -070025
26from xosconfig import Config
27from multistructlog import create_logger
Scott Baker8b725f52019-06-11 16:14:39 -070028from helpers import debug_once
Scott Baker3fd18e52018-04-17 09:18:21 -070029
Scott Baker3fd18e52018-04-17 09:18:21 -070030log = create_logger(Config().get('logging'))
31
Scott Baker8b725f52019-06-11 16:14:39 -070032
Scott Baker3fd18e52018-04-17 09:18:21 -070033class SyncService(SyncStep):
34
35 """
36 SyncService
37
38 Implements sync step for syncing Services.
39 """
40
41 provides = [Service]
42 observes = Service
43 requested_interval = 0
44
45 def __init__(self, *args, **kwargs):
46 super(SyncService, self).__init__(*args, **kwargs)
Scott Baker13e953c2018-05-17 09:19:15 -070047 self.init_kubernetes_client()
48
49 def init_kubernetes_client(self):
50 from kubernetes.client.rest import ApiException
51 from kubernetes import client as kubernetes_client, config as kubernetes_config
Scott Baker3fd18e52018-04-17 09:18:21 -070052 kubernetes_config.load_incluster_config()
Scott Baker13e953c2018-05-17 09:19:15 -070053 self.kubernetes_client = kubernetes_client
54 self.v1core = kubernetes_client.CoreV1Api()
55 self.ApiException = ApiException
Scott Baker3fd18e52018-04-17 09:18:21 -070056
57 def fetch_pending(self, deletion=False):
58 """ Filter the set of pending objects.
59 As this syncstep can only create Service that exist within Trust Domains, filter out those services that
60 don't have Trust Domains associated with them.
61 """
62 models = super(SyncService, self).fetch_pending(deletion)
63
64 if (not deletion):
65 for model in models[:]:
66 if not self.get_trust_domain(model):
Scott Baker04dd3e62018-11-27 17:17:13 -080067 # If this happens, then either the Service has no Slices, or it does have slices but none of
68 # those slices are associated with a TrustDomain. Assume the developer has done this on purpose
69 # and ignore the Service.
Scott Baker8b725f52019-06-11 16:14:39 -070070 debug_once("Service %s: Unable to determine Trust Domain. Ignoring." % model.name)
Scott Baker3fd18e52018-04-17 09:18:21 -070071 models.remove(model)
72 elif not model.serviceports.exists():
73 # If there are not ServicePorts, then there's not much for us to do at this time...
Scott Baker8b725f52019-06-11 16:14:39 -070074 debug_once("Service %s: Has no serviceports. Ignoring." % model.name)
Scott Baker3fd18e52018-04-17 09:18:21 -070075 models.remove(model)
76
77 return models
78
79 def get_trust_domain(self, o):
80 """ Given a service, determine its Trust Domain.
81
82 The design we've chosen to go with is that a service is pinned to a Trust Domain based on the slices
83 that it contains. It's an error for a service to be directly comprised of slices from multiple
84 trust domains.
85
86 This allows for "logical services", that contain no slices of their own, but are comprised of multiple
87 subservices. For example, EPC.
88 """
89
90 trust_domain = None
91 for slice in o.slices.all():
92 if slice.trust_domain:
93 if (trust_domain is None):
94 trust_domain = slice.trust_domain
95 elif (trust_domain.id != slice.trust_domain.id):
96 # Bail out of we've encountered a situation where a service spans multiple trust domains.
97 log.warning("Service %s is comprised of slices from multiple trust domains." % o.name)
98 return None
99
100 return trust_domain
101
Scott Baker393d0152018-05-21 09:17:49 -0700102 def get_service(self, o, trust_domain_name):
Scott Baker3fd18e52018-04-17 09:18:21 -0700103 """ Given an XOS Service, read the associated Service from Kubernetes.
104 If no Kubernetes service exists, return None
105 """
106 try:
Scott Baker393d0152018-05-21 09:17:49 -0700107 k8s_service = self.v1core.read_namespaced_service(o.name, trust_domain_name)
Scott Baker13e953c2018-05-17 09:19:15 -0700108 except self.ApiException, e:
Scott Baker3fd18e52018-04-17 09:18:21 -0700109 if e.status == 404:
110 return None
111 raise
112 return k8s_service
113
114 def sync_record(self, o):
115 trust_domain = self.get_trust_domain(o)
Scott Baker393d0152018-05-21 09:17:49 -0700116 k8s_service = self.get_service(o,trust_domain.name)
Scott Baker3fd18e52018-04-17 09:18:21 -0700117
118 if not k8s_service:
Scott Baker13e953c2018-05-17 09:19:15 -0700119 k8s_service = self.kubernetes_client.V1Service()
120 k8s_service.metadata = self.kubernetes_client.V1ObjectMeta(name=o.name)
Scott Baker3fd18e52018-04-17 09:18:21 -0700121
122 ports=[]
123 for service_port in o.serviceports.all():
Scott Baker13e953c2018-05-17 09:19:15 -0700124 port=self.kubernetes_client.V1ServicePort(name = service_port.name,
Scott Baker3fd18e52018-04-17 09:18:21 -0700125 node_port = service_port.external_port,
126 port = service_port.internal_port,
127 target_port = service_port.internal_port,
128 protocol = service_port.protocol)
129 ports.append(port)
130
Scott Baker13e953c2018-05-17 09:19:15 -0700131 k8s_service.spec = self.kubernetes_client.V1ServiceSpec(ports=ports,
Scott Baker3fd18e52018-04-17 09:18:21 -0700132 type="NodePort")
133
Scott Baker13e953c2018-05-17 09:19:15 -0700134 k8s_service = self.v1core.create_namespaced_service(trust_domain.name, k8s_service)
135
136 if (not o.backend_handle):
137 o.backend_handle = k8s_service.metadata.self_link
138 o.save(update_fields=["backend_handle"])
Scott Baker3fd18e52018-04-17 09:18:21 -0700139
140 def delete_record(self, o):
Scott Baker393d0152018-05-21 09:17:49 -0700141 trust_domain_name = None
142 trust_domain = self.get_trust_domain(o)
143 if trust_domain:
144 trust_domain_name = trust_domain.name
145 else:
146 # rely on backend_handle being structured like this one,
147 # /api/v1/namespaces/service1-trust/services/service1
148 if (o.backend_handle):
149 parts = o.backend_handle.split("/")
150 if len(parts)>3:
151 trust_domain_name = parts[-3]
Scott Baker3fd18e52018-04-17 09:18:21 -0700152
Scott Baker393d0152018-05-21 09:17:49 -0700153 if not trust_domain_name:
154 raise Exception("Can't delete service %s because there is no trust domain" % o.name)
155
156 k8s_service = self.get_service(o, trust_domain_name)
157 if not k8s_service:
158 log.info("Kubernetes service does not exist; Nothing to delete.", o=o)
159 return
160 delete_options = self.kubernetes_client.V1DeleteOptions()
161 self.v1core.delete_namespaced_service(o.name, trust_domain_name, delete_options)
162 log.info("Deleted service from kubernetes", handle=o.backend_handle)