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