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