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