Merge "Add new Kubernetes resource kind: "Network""
[yardstick.git] / yardstick / orchestrator / kubernetes.py
1 ##############################################################################
2 # Copyright (c) 2017 Huawei Technologies Co.,Ltd.
3 #
4 # All rights reserved. This program and the accompanying materials
5 # are made available under the terms of the Apache License, Version 2.0
6 # which accompanies this distribution, and is available at
7 # http://www.apache.org/licenses/LICENSE-2.0
8 ##############################################################################
9
10 import copy
11
12 from yardstick.common import constants
13 from yardstick.common import exceptions
14 from yardstick.common import utils
15 from yardstick.common import kubernetes_utils as k8s_utils
16
17
18 class ContainerObject(object):
19
20     SSH_MOUNT_PATH = '/tmp/.ssh/'
21     IMAGE_DEFAULT = 'openretriever/yardstick'
22     COMMAND_DEFAULT = '/bin/bash'
23
24     def __init__(self, name, ssh_key, **kwargs):
25         self._name = name
26         self._ssh_key = ssh_key
27         self._image = kwargs.get('image', self.IMAGE_DEFAULT)
28         self._command = [kwargs.get('command', self.COMMAND_DEFAULT)]
29         self._args = kwargs.get('args', [])
30         self._volume_mounts = kwargs.get('volumeMounts', [])
31         self._security_context = kwargs.get('securityContext')
32
33     def _create_volume_mounts(self):
34         """Return all "volumeMounts" items per container"""
35         volume_mounts_items = [self._create_volume_mounts_item(vol)
36                                for vol in self._volume_mounts]
37         ssh_vol = {'name': self._ssh_key,
38                    'mountPath': self.SSH_MOUNT_PATH}
39         volume_mounts_items.append(self._create_volume_mounts_item(ssh_vol))
40         return volume_mounts_items
41
42     @staticmethod
43     def _create_volume_mounts_item(volume_mount):
44         """Create a "volumeMounts" item"""
45         return {'name': volume_mount['name'],
46                 'mountPath': volume_mount['mountPath'],
47                 'readOnly': volume_mount.get('readOnly', False)}
48
49     def get_container_item(self):
50         """Create a "container" item"""
51         container_name = '{}-container'.format(self._name)
52         container = {'args': self._args,
53                      'command': self._command,
54                      'image': self._image,
55                      'name': container_name,
56                      'volumeMounts': self._create_volume_mounts()}
57         if self._security_context:
58             container['securityContext'] = self._security_context
59         return container
60
61
62 class KubernetesObject(object):
63
64     SSHKEY_DEFAULT = 'yardstick_key'
65
66     def __init__(self, name, **kwargs):
67         super(KubernetesObject, self).__init__()
68         parameters = copy.deepcopy(kwargs)
69         self.name = name
70         self.node_selector = parameters.pop('nodeSelector', {})
71         self.ssh_key = parameters.pop('ssh_key', self.SSHKEY_DEFAULT)
72         self._volumes = parameters.pop('volumes', [])
73         self._security_context = parameters.pop('securityContext', None)
74
75         containers = parameters.pop('containers', None)
76         if containers:
77             self._containers = [
78                 ContainerObject(self.name, self.ssh_key, **container)
79                 for container in containers]
80         else:
81             self._containers = [
82                 ContainerObject(self.name, self.ssh_key, **parameters)]
83
84         self.template = {
85             "apiVersion": "v1",
86             "kind": "ReplicationController",
87             "metadata": {
88                 "name": ""
89             },
90             "spec": {
91                 "replicas": 1,
92                 "template": {
93                     "metadata": {
94                         "labels": {"app": name}
95                     },
96                     "spec": {
97                         "containers": [],
98                         "volumes": [],
99                         "nodeSelector": {}
100                     }
101                 }
102             }
103         }
104
105         self._change_value_according_name(name)
106         self._add_containers()
107         self._add_node_selector()
108         self._add_volumes()
109         self._add_security_context()
110
111     def get_template(self):
112         return self.template
113
114     def _change_value_according_name(self, name):
115         utils.set_dict_value(self.template, 'metadata.name', name)
116
117         utils.set_dict_value(self.template,
118                              'spec.template.metadata.labels.app',
119                              name)
120
121     def _add_containers(self):
122         containers = [container.get_container_item()
123                       for container in self._containers]
124         utils.set_dict_value(self.template,
125                              'spec.template.spec.containers',
126                              containers)
127
128     def _add_node_selector(self):
129         utils.set_dict_value(self.template,
130                              'spec.template.spec.nodeSelector',
131                              self.node_selector)
132
133     def _add_volumes(self):
134         """Add "volume" items to container specs, including the SSH one"""
135         volume_items = [self._create_volume_item(vol) for vol in self._volumes]
136         volume_items.append(self._create_ssh_key_volume())
137         utils.set_dict_value(self.template,
138                              'spec.template.spec.volumes',
139                              volume_items)
140
141     def _create_ssh_key_volume(self):
142         """Create a "volume" item of type "configMap" for the SSH key"""
143         return {'name': self.ssh_key,
144                 'configMap': {'name': self.ssh_key}}
145
146     @staticmethod
147     def _create_volume_item(volume):
148         """Create a "volume" item"""
149         volume = copy.deepcopy(volume)
150         name = volume.pop('name')
151         for key in (k for k in volume if k in k8s_utils.get_volume_types()):
152             type_name = key
153             type_data = volume[key]
154             break
155         else:
156             raise exceptions.KubernetesTemplateInvalidVolumeType(volume=volume)
157
158         return {'name': name,
159                 type_name: type_data}
160
161     def _add_security_context(self):
162         if self._security_context:
163             utils.set_dict_value(self.template,
164                                  'spec.template.spec.securityContext',
165                                  self._security_context)
166
167
168 class ServiceObject(object):
169
170     def __init__(self, name):
171         self.name = '{}-service'.format(name)
172         self.template = {
173             'metadata': {
174                 'name': '{}-service'.format(name)
175             },
176             'spec': {
177                 'type': 'NodePort',
178                 'ports': [
179                     {
180                         'port': 22,
181                         'protocol': 'TCP'
182                     }
183                 ],
184                 'selector': {
185                     'app': name
186                 }
187             }
188         }
189
190     def create(self):
191         k8s_utils.create_service(self.template)
192
193     def delete(self):
194         k8s_utils.delete_service(self.name)
195
196
197 class CustomResourceDefinitionObject(object):
198
199     MANDATORY_PARAMETERS = {'name'}
200
201     def __init__(self, ctx_name, **kwargs):
202         if not self.MANDATORY_PARAMETERS.issubset(kwargs):
203             missing_parameters = ', '.join(
204                 str(param) for param in
205                 (self.MANDATORY_PARAMETERS - set(kwargs)))
206             raise exceptions.KubernetesCRDObjectDefinitionError(
207                 missing_parameters=missing_parameters)
208
209         singular = kwargs['name']
210         plural = singular + 's'
211         kind = singular.title()
212         version = kwargs.get('version', 'v1')
213         scope = kwargs.get('scope', constants.SCOPE_NAMESPACED)
214         group = ctx_name + '.com'
215         self._name = metadata_name = plural + '.' + group
216
217         self._template = {
218             'metadata': {
219                 'name': metadata_name
220             },
221             'spec': {
222                 'group': group,
223                 'version': version,
224                 'scope': scope,
225                 'names': {'plural': plural,
226                           'singular': singular,
227                           'kind': kind}
228             }
229         }
230
231     def create(self):
232         k8s_utils.create_custom_resource_definition(self._template)
233
234     def delete(self):
235         k8s_utils.delete_custom_resource_definition(self._name)
236
237
238 class NetworkObject(object):
239
240     MANDATORY_PARAMETERS = {'name', 'plugin', 'args'}
241     KIND = 'Network'
242
243     def __init__(self, **kwargs):
244         if not self.MANDATORY_PARAMETERS.issubset(kwargs):
245             missing_parameters = ', '.join(
246                 str(param) for param in
247                 (self.MANDATORY_PARAMETERS - set(kwargs)))
248             raise exceptions.KubernetesNetworkObjectDefinitionError(
249                 missing_parameters=missing_parameters)
250
251         self._name = kwargs['name']
252         self._plugin = kwargs['plugin']
253         self._args = kwargs['args']
254         self._crd = None
255         self._template = None
256         self._group = None
257         self._version = None
258         self._plural = None
259         self._scope = None
260
261     @property
262     def crd(self):
263         if self._crd:
264             return self._crd
265         crd = k8s_utils.get_custom_resource_definition(self.KIND)
266         if not crd:
267             raise exceptions.KubernetesNetworkObjectKindMissing()
268         self._crd = crd
269         return self._crd
270
271     @property
272     def group(self):
273         if self._group:
274             return self._group
275         self._group = self.crd.spec.group
276         return self._group
277
278     @property
279     def version(self):
280         if self._version:
281             return self._version
282         self._version = self.crd.spec.version
283         return self._version
284
285     @property
286     def plural(self):
287         if self._plural:
288             return self._plural
289         self._plural = self.crd.spec.names.plural
290         return self._plural
291
292     @property
293     def scope(self):
294         if self._scope:
295             return self._scope
296         self._scope = self.crd.spec.scope
297         return self._scope
298
299     @property
300     def template(self):
301         """"Network" object template
302
303         This template can be rendered only once the CRD "Network" is created in
304         Kubernetes. This function call must be delayed until the creation of
305         the CRD "Network".
306         """
307         if self._template:
308             return self._template
309
310         self._template = {
311             'apiVersion': '{}/{}'.format(self.group, self.version),
312             'kind': self.KIND,
313             'metadata': {
314                 'name': self._name
315             },
316             'plugin': self._plugin,
317             'args': self._args
318         }
319         return self._template
320
321     def create(self):
322         k8s_utils.create_network(self.scope, self.group, self.version,
323                                  self.plural, self.template)
324
325     def delete(self):
326         k8s_utils.delete_network(self.scope, self.group, self.version,
327                                  self.plural, self._name)
328
329
330 class KubernetesTemplate(object):
331
332     def __init__(self, name, context_cfg):
333         """KubernetesTemplate object initialization
334
335         :param name: (str) name of the Kubernetes context
336         :param context_cfg: (dict) context definition
337         """
338         context_cfg = copy.deepcopy(context_cfg)
339         servers_cfg = context_cfg.pop('servers', {})
340         crd_cfg = context_cfg.pop('custom_resources', [])
341         networks_cfg = context_cfg.pop('networks', [])
342         self.name = name
343         self.ssh_key = '{}-key'.format(name)
344
345         self.rcs = [self._get_rc_name(rc) for rc in servers_cfg]
346         self.k8s_objs = [KubernetesObject(self._get_rc_name(rc),
347                                           ssh_key=self.ssh_key,
348                                           **cfg)
349                          for rc, cfg in servers_cfg.items()]
350         self.service_objs = [ServiceObject(s) for s in self.rcs]
351         self.crd = [CustomResourceDefinitionObject(self.name, **crd)
352                     for crd in crd_cfg]
353         self.network_objs = [NetworkObject(**nobj) for nobj in networks_cfg]
354         self.pods = []
355
356     def _get_rc_name(self, rc_name):
357         return '{}-{}'.format(rc_name, self.name)
358
359     def get_rc_pods(self):
360         resp = k8s_utils.get_pod_list()
361         self.pods = [p.metadata.name for p in resp.items for s in self.rcs
362                      if p.metadata.name.startswith(s)]
363
364         return self.pods