Browse Source

Update node-labels through Kubernetes Provisioner

Blueprint: https://airshipit.readthedocs.io/projects/specs/en/latest/specs/approved/k8s_update_node_labels_workflow.html

This commit adds:

1. A new task action ''relabel_nodes'' added to update
nodes labels
2.A new Kubernetes driver added for Kubernetes cluster
interaction through Promenade.

Change-Id: I37c2d7bfda4966d907556036bc2b343df451994c
Soumitra Khuntia 8 months ago
parent
commit
f879e3a88d

+ 9
- 0
docs/source/_static/drydock.conf.sample View File

@@ -276,6 +276,9 @@
276 276
 # Logger name for Node driver logging (string value)
277 277
 #nodedriver_logger_name = ${global_logger_name}.nodedriver
278 278
 
279
+# Logger name for Kubernetes driver logging (string value)
280
+#kubernetesdriver_logger_name = ${global_logger_name}.kubernetesdriver
281
+
279 282
 # Logger name for API server logging (string value)
280 283
 #control_logger_name = ${global_logger_name}.control
281 284
 
@@ -350,6 +353,9 @@
350 353
 # Module path string of the Node driver to enable (string value)
351 354
 #node_driver = drydock_provisioner.drivers.node.maasdriver.driver.MaasNodeDriver
352 355
 
356
+# Module path string of the Kubernetes driver to enable (string value)
357
+#kubernetes_driver = drydock_provisioner.drivers.kubernetes.promenade_driver.driver.PromenadeDriver
358
+
353 359
 # Module path string of the Network driver enable (string value)
354 360
 #network_driver = <None>
355 361
 
@@ -398,6 +404,9 @@
398 404
 # Timeout in minutes for deploying a node (integer value)
399 405
 #deploy_node = 45
400 406
 
407
+# Timeout in minutes for relabeling a node (integer value)
408
+#relabel_node = 5
409
+
401 410
 # Timeout in minutes between deployment completion and the all boot actions
402 411
 # reporting status (integer value)
403 412
 #bootaction_final_status = 15

+ 4
- 0
docs/source/_static/policy.yaml.sample View File

@@ -38,6 +38,10 @@
38 38
 # POST  /api/v1.0/tasks
39 39
 #"physical_provisioner:destroy_nodes": "role:admin"
40 40
 
41
+# Create relabel_nodes task
42
+# POST  /api/v1.0/tasks
43
+#"physical_provisioner:relabel_nodes": "role:admin"
44
+
41 45
 # Read build data for a node
42 46
 # GET  /api/v1.0/nodes/{nodename}/builddata
43 47
 #"physical_provisioner:read_build_data": "role:admin"

+ 2
- 2
docs/source/task.rst View File

@@ -14,7 +14,7 @@ Task Document Schema
14 14
 This document can be posted to the Drydock :ref:`tasks-api` to create a new task.::
15 15
 
16 16
     {
17
-      "action": "validate_design|verify_site|prepare_site|verify_node|prepare_node|deploy_node|destroy_node",
17
+      "action": "validate_design|verify_site|prepare_site|verify_node|prepare_node|deploy_node|destroy_node|relabel_nodes",
18 18
       "design_ref": "http_uri|deckhand_uri|file_uri",
19 19
       "node_filter": {
20 20
         "filter_set_type": "intersection|union",
@@ -90,7 +90,7 @@ When querying the state of an existing task, the below document will be returned
90 90
       "Kind": "Task",
91 91
       "apiVersion": "v1.0",
92 92
       "task_id": "uuid",
93
-      "action": "validate_design|verify_site|prepare_site|verify_node|prepare_node|deploy_node|destroy_node",
93
+      "action": "validate_design|verify_site|prepare_site|verify_node|prepare_node|deploy_node|destroy_node|relabel_nodes",
94 94
       "design_ref": "http_uri|deckhand_uri|file_uri",
95 95
       "parent_task_id": "uuid",
96 96
       "subtask_id_list": ["uuid","uuid",...],

+ 13
- 0
python/drydock_provisioner/config.py View File

@@ -81,6 +81,10 @@ class DrydockConfig(object):
81 81
             'nodedriver_logger_name',
82 82
             default='${global_logger_name}.nodedriver',
83 83
             help='Logger name for Node driver logging'),
84
+        cfg.StrOpt(
85
+            'kubernetesdriver_logger_name',
86
+            default='${global_logger_name}.kubernetesdriver',
87
+            help='Logger name for Kubernetes driver logging'),
84 88
         cfg.StrOpt(
85 89
             'control_logger_name',
86 90
             default='${global_logger_name}.control',
@@ -166,6 +170,11 @@ class DrydockConfig(object):
166 170
             default=
167 171
             'drydock_provisioner.drivers.node.maasdriver.driver.MaasNodeDriver',
168 172
             help='Module path string of the Node driver to enable'),
173
+        cfg.StrOpt(
174
+            'kubernetes_driver',
175
+            default=
176
+            'drydock_provisioner.drivers.kubernetes.promenade_driver.driver.PromenadeDriver',
177
+            help='Module path string of the Kubernetes driver to enable'),
169 178
         # TODO(sh8121att) Network driver not yet implemented
170 179
         cfg.StrOpt(
171 180
             'network_driver',
@@ -224,6 +233,10 @@ class DrydockConfig(object):
224 233
             default=30,
225 234
             help='Timeout in minutes for releasing a node',
226 235
         ),
236
+        cfg.IntOpt(
237
+            'relabel_node',
238
+            default=5,
239
+            help='Timeout in minutes for relabeling a node'),
227 240
     ]
228 241
 
229 242
     def __init__(self):

+ 25
- 0
python/drydock_provisioner/control/tasks.py View File

@@ -63,6 +63,7 @@ class TasksResource(StatefulResource):
63 63
             'prepare_nodes': TasksResource.task_prepare_nodes,
64 64
             'deploy_nodes': TasksResource.task_deploy_nodes,
65 65
             'destroy_nodes': TasksResource.task_destroy_nodes,
66
+            'relabel_nodes': TasksResource.task_relabel_nodes,
66 67
         }
67 68
 
68 69
         try:
@@ -253,6 +254,30 @@ class TasksResource(StatefulResource):
253 254
             self.return_error(
254 255
                 resp, falcon.HTTP_400, message=ex.msg, retry=False)
255 256
 
257
+    @policy.ApiEnforcer('physical_provisioner:relabel_nodes')
258
+    def task_relabel_nodes(self, req, resp, json_data):
259
+        """Create async task for relabel nodes."""
260
+        action = json_data.get('action', None)
261
+
262
+        if action != 'relabel_nodes':
263
+            self.error(
264
+                req.context,
265
+                "Task body ended up in wrong handler: action %s in task_relabel_nodes"
266
+                % action)
267
+            self.return_error(
268
+                resp, falcon.HTTP_500, message="Error", retry=False)
269
+
270
+        try:
271
+            task = self.create_task(json_data, req.context)
272
+            resp.body = json.dumps(task.to_dict())
273
+            resp.append_header('Location',
274
+                               "/api/v1.0/tasks/%s" % str(task.task_id))
275
+            resp.status = falcon.HTTP_201
276
+        except errors.InvalidFormat as ex:
277
+            self.error(req.context, ex.msg)
278
+            self.return_error(
279
+                resp, falcon.HTTP_400, message=ex.msg, retry=False)
280
+
256 281
     def create_task(self, task_body, req_context):
257 282
         """General task creation.
258 283
 

+ 14
- 0
python/drydock_provisioner/drivers/kubernetes/__init__.py View File

@@ -0,0 +1,14 @@
1
+# Copyright 2018 AT&T Intellectual Property.  All other rights reserved.
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain a copy of the License at
6
+#
7
+#     http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+# See the License for the specific language governing permissions and
13
+# limitations under the License.
14
+"""Drivers for use for Kubernetes provisioner interaction."""

+ 46
- 0
python/drydock_provisioner/drivers/kubernetes/driver.py View File

@@ -0,0 +1,46 @@
1
+# Copyright 2018 AT&T Intellectual Property.  All other rights reserved.
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain a copy of the License at
6
+#
7
+#     http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+# See the License for the specific language governing permissions and
13
+# limitations under the License.
14
+"""Generic driver for Kubernetes Interaction."""
15
+
16
+import drydock_provisioner.objects.fields as hd_fields
17
+import drydock_provisioner.error as errors
18
+
19
+from drydock_provisioner.drivers.driver import ProviderDriver
20
+
21
+
22
+class KubernetesDriver(ProviderDriver):
23
+
24
+    driver_name = "Kubernetes_generic"
25
+    driver_key = "Kubernetes_generic"
26
+    driver_desc = "Generic Kubernetes Driver"
27
+
28
+    def __init__(self, **kwargs):
29
+        super(KubernetesDriver, self).__init__(**kwargs)
30
+
31
+        self.supported_actions = [
32
+            hd_fields.OrchestratorAction.RelabelNode,
33
+        ]
34
+
35
+    def execute_task(self, task_id):
36
+        task = self.state_manager.get_task(task_id)
37
+        task_action = task.action
38
+
39
+        if task_action in self.supported_actions:
40
+            task.success()
41
+            task.set_status(hd_fields.TaskStatus.Complete)
42
+            task.save()
43
+            return
44
+        else:
45
+            raise errors.DriverError("Unsupported action %s for driver %s" %
46
+                                     (task_action, self.driver_desc))

+ 4
- 0
python/drydock_provisioner/drivers/kubernetes/promenade_driver/README.md View File

@@ -0,0 +1,4 @@
1
+# Promenade Kubernetes Driver #
2
+
3
+This driver will handle the interaction with Promenade for
4
+any changes applied to Kubernetes cluster nodes.

+ 14
- 0
python/drydock_provisioner/drivers/kubernetes/promenade_driver/__init__.py View File

@@ -0,0 +1,14 @@
1
+# Copyright 2018 AT&T Intellectual Property.  All other rights reserved.
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain a copy of the License at
6
+#
7
+#     http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+# See the License for the specific language governing permissions and
13
+# limitations under the License.
14
+"""Drivers for use for Promenade interaction."""

+ 14
- 0
python/drydock_provisioner/drivers/kubernetes/promenade_driver/actions/__init__.py View File

@@ -0,0 +1,14 @@
1
+# Copyright 2018 AT&T Intellectual Property.  All other rights reserved.
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain a copy of the License at
6
+#
7
+#     http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+# See the License for the specific language governing permissions and
13
+# limitations under the License.
14
+"""Kubernetes task driver action for Promenade interaction."""

+ 83
- 0
python/drydock_provisioner/drivers/kubernetes/promenade_driver/actions/k8s_node.py View File

@@ -0,0 +1,83 @@
1
+# Copyright 2018 AT&T Intellectual Property.  All other rights reserved.
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain a copy of the License at
6
+#
7
+#     http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+# See the License for the specific language governing permissions and
13
+# limitations under the License.
14
+"""Task driver for Promenade interaction."""
15
+
16
+import logging
17
+
18
+import drydock_provisioner.error as errors
19
+import drydock_provisioner.config as config
20
+import drydock_provisioner.objects.fields as hd_fields
21
+from drydock_provisioner.orchestrator.actions.orchestrator import BaseAction
22
+
23
+
24
+class PromenadeAction(BaseAction):
25
+    def __init__(self, *args, prom_client=None):
26
+        super().__init__(*args)
27
+
28
+        self.promenade_client = prom_client
29
+
30
+        self.logger = logging.getLogger(
31
+            config.config_mgr.conf.logging.kubernetesdriver_logger_name)
32
+
33
+
34
+class RelabelNode(PromenadeAction):
35
+    """Action to relabel kubernetes node."""
36
+
37
+    def start(self):
38
+
39
+        self.task.set_status(hd_fields.TaskStatus.Running)
40
+        self.task.save()
41
+
42
+        try:
43
+            site_design = self._load_site_design()
44
+        except errors.OrchestratorError:
45
+            self.task.add_status_msg(
46
+                msg="Error loading site design.",
47
+                error=True,
48
+                ctx='NA',
49
+                ctx_type='NA')
50
+            self.task.set_status(hd_fields.TaskStatus.Complete)
51
+            self.task.failure()
52
+            self.task.save()
53
+            return
54
+
55
+        nodes = self.orchestrator.process_node_filter(self.task.node_filter,
56
+                                                      site_design)
57
+
58
+        for n in nodes:
59
+            # Relabel node through Promenade
60
+            try:
61
+                self.logger.info("Relabeling node %s with node label data." % n.name)
62
+
63
+                labels_dict = n.get_node_labels()
64
+                msg = "Set labels %s for node %s" % (str(labels_dict), n.name)
65
+
66
+                self.task.add_status_msg(
67
+                    msg=msg, error=False, ctx=n.name, ctx_type='node')
68
+
69
+                # Call promenade to invoke relabel node
70
+                self.promenade_client.relabel_node(n.get_id(), labels_dict)
71
+                self.task.success(focus=n.get_id())
72
+            except Exception as ex:
73
+                msg = "Error relabeling node %s with label data" % n.name
74
+                self.logger.warning(msg + ": " + str(ex))
75
+                self.task.failure(focus=n.get_id())
76
+                self.task.add_status_msg(
77
+                    msg=msg, error=True, ctx=n.name, ctx_type='node')
78
+                continue
79
+
80
+        self.task.set_status(hd_fields.TaskStatus.Complete)
81
+        self.task.save()
82
+
83
+        return

+ 156
- 0
python/drydock_provisioner/drivers/kubernetes/promenade_driver/driver.py View File

@@ -0,0 +1,156 @@
1
+# Copyright 2018 AT&T Intellectual Property.  All other rights reserved.
2
+# Licensed under the Apache License, Version 2.0 (the "License");
3
+# you may not use this file except in compliance with the License.
4
+# You may obtain a copy of the License at
5
+#
6
+#     http://www.apache.org/licenses/LICENSE-2.0
7
+#
8
+# Unless required by applicable law or agreed to in writing, software
9
+# distributed under the License is distributed on an "AS IS" BASIS,
10
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+# See the License for the specific language governing permissions and
12
+# limitations under the License.
13
+"""Task driver for Promenade"""
14
+
15
+import logging
16
+import uuid
17
+import concurrent.futures
18
+
19
+from oslo_config import cfg
20
+
21
+import drydock_provisioner.error as errors
22
+import drydock_provisioner.objects.fields as hd_fields
23
+import drydock_provisioner.config as config
24
+from drydock_provisioner.drivers.kubernetes.driver import KubernetesDriver
25
+from drydock_provisioner.drivers.kubernetes.promenade_driver.promenade_client \
26
+    import PromenadeClient
27
+
28
+from .actions.k8s_node import RelabelNode
29
+
30
+
31
+class PromenadeDriver(KubernetesDriver):
32
+
33
+    driver_name = 'promenadedriver'
34
+    driver_key = 'promenadedriver'
35
+    driver_desc = 'Promenade Kubernetes Driver'
36
+
37
+    action_class_map = {
38
+        hd_fields.OrchestratorAction.RelabelNode:
39
+        RelabelNode,
40
+    }
41
+
42
+    def __init__(self, **kwargs):
43
+        super().__init__(**kwargs)
44
+
45
+        self.logger = logging.getLogger(
46
+            cfg.CONF.logging.kubernetesdriver_logger_name)
47
+
48
+    def execute_task(self, task_id):
49
+        # actions that should be threaded for execution
50
+        threaded_actions = [
51
+            hd_fields.OrchestratorAction.RelabelNode,
52
+        ]
53
+
54
+        action_timeouts = {
55
+            hd_fields.OrchestratorAction.RelabelNode:
56
+            config.config_mgr.conf.timeouts.relabel_node,
57
+        }
58
+
59
+        task = self.state_manager.get_task(task_id)
60
+
61
+        if task is None:
62
+            raise errors.DriverError("Invalid task %s" % (task_id))
63
+
64
+        if task.action not in self.supported_actions:
65
+            raise errors.DriverError("Driver %s doesn't support task action %s"
66
+                                     % (self.driver_desc, task.action))
67
+
68
+        task.set_status(hd_fields.TaskStatus.Running)
69
+        task.save()
70
+
71
+        if task.action in threaded_actions:
72
+            if task.retry > 0:
73
+                msg = "Retrying task %s on previous failed entities." % str(
74
+                    task.get_id())
75
+                task.add_status_msg(
76
+                    msg=msg,
77
+                    error=False,
78
+                    ctx=str(task.get_id()),
79
+                    ctx_type='task')
80
+                target_nodes = self.orchestrator.get_target_nodes(
81
+                    task, failures=True)
82
+            else:
83
+                target_nodes = self.orchestrator.get_target_nodes(task)
84
+
85
+            with concurrent.futures.ThreadPoolExecutor() as e:
86
+                subtask_futures = dict()
87
+                for n in target_nodes:
88
+                    prom_client = PromenadeClient()
89
+                    nf = self.orchestrator.create_nodefilter_from_nodelist([n])
90
+                    subtask = self.orchestrator.create_task(
91
+                        design_ref=task.design_ref,
92
+                        action=task.action,
93
+                        node_filter=nf,
94
+                        retry=task.retry)
95
+                    task.register_subtask(subtask)
96
+
97
+                    action = self.action_class_map.get(task.action, None)(
98
+                        subtask,
99
+                        self.orchestrator,
100
+                        self.state_manager,
101
+                        prom_client=prom_client)
102
+                    subtask_futures[subtask.get_id().bytes] = e.submit(
103
+                        action.start)
104
+
105
+                timeout = action_timeouts.get(
106
+                    task.action,
107
+                    config.config_mgr.conf.timeouts.relabel_node)
108
+                finished, running = concurrent.futures.wait(
109
+                    subtask_futures.values(), timeout=(timeout * 60))
110
+
111
+            for t, f in subtask_futures.items():
112
+                if not f.done():
113
+                    task.add_status_msg(
114
+                        "Subtask timed out before completing.",
115
+                        error=True,
116
+                        ctx=str(uuid.UUID(bytes=t)),
117
+                        ctx_type='task')
118
+                    task.failure()
119
+                else:
120
+                    if f.exception():
121
+                        msg = ("Subtask %s raised unexpected exception: %s"
122
+                               % (str(uuid.UUID(bytes=t)), str(f.exception())))
123
+                        self.logger.error(msg, exc_info=f.exception())
124
+                        task.add_status_msg(
125
+                            msg=msg,
126
+                            error=True,
127
+                            ctx=str(uuid.UUID(bytes=t)),
128
+                            ctx_type='task')
129
+                        task.failure()
130
+
131
+            task.bubble_results()
132
+            task.align_result()
133
+        else:
134
+            try:
135
+                prom_client = PromenadeClient()
136
+                action = self.action_class_map.get(task.action, None)(
137
+                    task,
138
+                    self.orchestrator,
139
+                    self.state_manager,
140
+                    prom_client=prom_client)
141
+                action.start()
142
+            except Exception as e:
143
+                msg = ("Subtask for action %s raised unexpected exception: %s"
144
+                       % (task.action, str(e)))
145
+                self.logger.error(msg, exc_info=e)
146
+                task.add_status_msg(
147
+                    msg=msg,
148
+                    error=True,
149
+                    ctx=str(task.get_id()),
150
+                    ctx_type='task')
151
+                task.failure()
152
+
153
+        task.set_status(hd_fields.TaskStatus.Complete)
154
+        task.save()
155
+
156
+        return

+ 296
- 0
python/drydock_provisioner/drivers/kubernetes/promenade_driver/promenade_client.py View File

@@ -0,0 +1,296 @@
1
+# Copyright 2018 AT&T Intellectual Property.  All other rights reserved.
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain a copy of the License at
6
+#
7
+#     http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+# See the License for the specific language governing permissions and
13
+# limitations under the License.
14
+"""Client for submitting authenticated requests to Promenade API."""
15
+
16
+import logging
17
+import requests
18
+from urllib.parse import urlparse
19
+
20
+from keystoneauth1 import exceptions as exc
21
+
22
+import drydock_provisioner.error as errors
23
+from drydock_provisioner.util import KeystoneUtils
24
+
25
+# TODO: Remove this local implementation of Promenade Session and client once
26
+# Promenade api client is available as part of Promenade project.
27
+class PromenadeSession(object):
28
+    """
29
+    A session to the Promenade API maintaining credentials and API options
30
+
31
+    :param string marker: (optional) external context marker
32
+    :param tuple timeout: (optional) a tuple of connect, read timeout values
33
+        to use as the default for invocations using this session. A single
34
+        value may also be supplied instead of a tuple to indicate only the
35
+        read timeout to use
36
+    """
37
+
38
+    def __init__(self,
39
+                 scheme='http',
40
+                 marker=None,
41
+                 timeout=None):
42
+        self.logger = logging.getLogger(__name__)
43
+        self.__session = requests.Session()
44
+
45
+        self.set_auth()
46
+
47
+        self.marker = marker
48
+        self.__session.headers.update({'X-Context-Marker': marker})
49
+
50
+        self.prom_url = self._get_prom_url()
51
+        self.port = self.prom_url.port
52
+        self.host = self.prom_url.hostname
53
+        self.scheme = scheme
54
+
55
+        if self.port:
56
+            self.base_url = "%s://%s:%s/api/" % (self.scheme, self.host,
57
+                                                 self.port)
58
+        else:
59
+            # assume default port for scheme
60
+            self.base_url = "%s://%s/api/" % (self.scheme, self.host)
61
+
62
+        self.default_timeout = self._calc_timeout_tuple((20, 30), timeout)
63
+
64
+    def set_auth(self):
65
+
66
+            auth_header = self._auth_gen()
67
+            self.__session.headers.update(auth_header)
68
+
69
+    def get(self, route, query=None, timeout=None):
70
+        """
71
+        Send a GET request to Promenade.
72
+
73
+        :param string route: The URL string following the hostname and API prefix
74
+        :param dict query: A dict of k, v pairs to add to the query string
75
+        :param timeout: A single or tuple value for connect, read timeout.
76
+            A single value indicates the read timeout only
77
+        :return: A requests.Response object
78
+        """
79
+        auth_refresh = False
80
+        while True:
81
+            url = self.base_url + route
82
+            self.logger.debug('GET ' + url)
83
+            self.logger.debug('Query Params: ' + str(query))
84
+            resp = self.__session.get(
85
+                url, params=query, timeout=self._timeout(timeout))
86
+
87
+            if resp.status_code == 401 and not auth_refresh:
88
+                self.set_auth()
89
+                auth_refresh = True
90
+            else:
91
+                break
92
+
93
+        return resp
94
+
95
+    def put(self, endpoint, query=None, body=None, data=None, timeout=None):
96
+        """
97
+        Send a PUT request to Promenade. If both body and data are specified,
98
+        body will be used.
99
+
100
+        :param string endpoint: The URL string following the hostname and API prefix
101
+        :param dict query: A dict of k, v parameters to add to the query string
102
+        :param string body: A string to use as the request body. Will be treated as raw
103
+        :param data: Something json.dumps(s) can serialize. Result will be used as the request body
104
+        :param timeout: A single or tuple value for connect, read timeout.
105
+            A single value indicates the read timeout only
106
+        :return: A requests.Response object
107
+        """
108
+        auth_refresh = False
109
+        url = self.base_url + endpoint
110
+        while True:
111
+            self.logger.debug('PUT ' + url)
112
+            self.logger.debug('Query Params: ' + str(query))
113
+            if body is not None:
114
+                self.logger.debug(
115
+                    "Sending PUT with explicit body: \n%s" % body)
116
+                resp = self.__session.put(
117
+                    self.base_url + endpoint,
118
+                    params=query,
119
+                    data=body,
120
+                    timeout=self._timeout(timeout))
121
+            else:
122
+                self.logger.debug(
123
+                    "Sending PUT with JSON body: \n%s" % str(data))
124
+                resp = self.__session.put(
125
+                    self.base_url + endpoint,
126
+                    params=query,
127
+                    json=data,
128
+                    timeout=self._timeout(timeout))
129
+            if resp.status_code == 401 and not auth_refresh:
130
+                self.set_auth()
131
+                auth_refresh = True
132
+            else:
133
+                break
134
+
135
+        return resp
136
+
137
+    def post(self, endpoint, query=None, body=None, data=None, timeout=None):
138
+        """
139
+        Send a POST request to Drydock. If both body and data are specified,
140
+        body will be used.
141
+
142
+        :param string endpoint: The URL string following the hostname and API prefix
143
+        :param dict query: A dict of k, v parameters to add to the query string
144
+        :param string body: A string to use as the request body. Will be treated as raw
145
+        :param data: Something json.dumps(s) can serialize. Result will be used as the request body
146
+        :param timeout: A single or tuple value for connect, read timeout.
147
+            A single value indicates the read timeout only
148
+        :return: A requests.Response object
149
+        """
150
+        auth_refresh = False
151
+        url = self.base_url + endpoint
152
+        while True:
153
+            self.logger.debug('POST ' + url)
154
+            self.logger.debug('Query Params: ' + str(query))
155
+            if body is not None:
156
+                self.logger.debug(
157
+                    "Sending POST with explicit body: \n%s" % body)
158
+                resp = self.__session.post(
159
+                    self.base_url + endpoint,
160
+                    params=query,
161
+                    data=body,
162
+                    timeout=self._timeout(timeout))
163
+            else:
164
+                self.logger.debug(
165
+                    "Sending POST with JSON body: \n%s" % str(data))
166
+                resp = self.__session.post(
167
+                    self.base_url + endpoint,
168
+                    params=query,
169
+                    json=data,
170
+                    timeout=self._timeout(timeout))
171
+            if resp.status_code == 401 and not auth_refresh:
172
+                self.set_auth()
173
+                auth_refresh = True
174
+            else:
175
+                break
176
+
177
+        return resp
178
+
179
+    def _timeout(self, timeout=None):
180
+        """Calculate the default timeouts for this session
181
+
182
+        :param timeout: A single or tuple value for connect, read timeout.
183
+            A single value indicates the read timeout only
184
+        :return: the tuple of the default timeouts used for this session
185
+        """
186
+        return self._calc_timeout_tuple(self.default_timeout, timeout)
187
+
188
+    def _calc_timeout_tuple(self, def_timeout, timeout=None):
189
+        """Calculate the default timeouts for this session
190
+
191
+        :param def_timeout: The default timeout tuple to be used if no specific
192
+            timeout value is supplied
193
+        :param timeout: A single or tuple value for connect, read timeout.
194
+            A single value indicates the read timeout only
195
+        :return: the tuple of the timeouts calculated
196
+        """
197
+        connect_timeout, read_timeout = def_timeout
198
+
199
+        try:
200
+            if isinstance(timeout, tuple):
201
+                if all(isinstance(v, int)
202
+                       for v in timeout) and len(timeout) == 2:
203
+                    connect_timeout, read_timeout = timeout
204
+                else:
205
+                    raise ValueError("Tuple non-integer or wrong length")
206
+            elif isinstance(timeout, int):
207
+                read_timeout = timeout
208
+            elif timeout is not None:
209
+                raise ValueError("Non integer timeout value")
210
+        except ValueError:
211
+            self.logger.warn(
212
+                "Timeout value must be a tuple of integers or a "
213
+                "single integer. Proceeding with values of "
214
+                "(%s, %s)", connect_timeout, read_timeout)
215
+        return (connect_timeout, read_timeout)
216
+
217
+    def _get_ks_session(self):
218
+        # Get keystone session object
219
+
220
+        try:
221
+            ks_session = KeystoneUtils.get_session()
222
+        except exc.AuthorizationFailure as aferr:
223
+            self.logger.error(
224
+                'Could not authorize against Keystone: %s',
225
+                str(aferr))
226
+            raise errors.DriverError('Could not authorize against Keystone: %s',
227
+                                     str(aferr))
228
+
229
+        return ks_session
230
+
231
+    def _get_prom_url(self):
232
+        # Get promenade url from Keystone session object
233
+
234
+        ks_session = self._get_ks_session()
235
+
236
+        try:
237
+            prom_endpoint = ks_session.get_endpoint(
238
+                interface='internal',
239
+                service_type='kubernetesprovisioner')
240
+        except exc.EndpointNotFound:
241
+            self.logger.error("Could not find an internal interface"
242
+                              " defined in Keystone for Promenade")
243
+
244
+            raise errors.DriverError("Could not find an internal interface"
245
+                                     " defined in Keystone for Promenade")
246
+
247
+        prom_url = urlparse(prom_endpoint)
248
+
249
+        return prom_url
250
+
251
+    def _auth_gen(self):
252
+        # Get auth token from Keystone session
253
+        token = self._get_ks_session().get_auth_headers().get('X-Auth-Token')
254
+        return [('X-Auth-Token', token)]
255
+
256
+
257
+class PromenadeClient(object):
258
+    """"
259
+    A client for the Promenade API
260
+    """
261
+
262
+    def __init__(self):
263
+        self.session = PromenadeSession()
264
+        self.logger = logging.getLogger(__name__)
265
+
266
+    def relabel_node(self, node_id, node_labels):
267
+        """ Relabel kubernetes node
268
+
269
+        :param string node_id: Node id for node to be relabeled.
270
+        :param dict node_labels: The dictionary representation of node labels
271
+                                 that needs be re-applied to the node.
272
+        :return: response
273
+        """
274
+
275
+        route = 'v1.0/node-labels/{}'.format(node_id)
276
+
277
+        self.logger.debug("promenade_client is calling %s API: body is %s" %
278
+                          (route, str(node_labels)))
279
+
280
+        resp = self.session.put(route, data=node_labels)
281
+
282
+        self._check_response(resp)
283
+
284
+        return resp.json()
285
+
286
+    def _check_response(self, resp):
287
+        if resp.status_code == 401:
288
+            raise errors.ClientUnauthorizedError(
289
+                "Unauthorized access to %s, include valid token." % resp.url)
290
+        elif resp.status_code == 403:
291
+            raise errors.ClientForbiddenError(
292
+                "Forbidden access to %s" % resp.url)
293
+        elif not resp.ok:
294
+            raise errors.ClientError(
295
+                "Error - received %d: %s" % (resp.status_code, resp.text),
296
+                code=resp.status_code)

+ 10
- 6
python/drydock_provisioner/objects/fields.py View File

@@ -31,6 +31,7 @@ class OrchestratorAction(BaseDrydockEnum):
31 31
     DeployNodes = 'deploy_nodes'
32 32
     DestroyNodes = 'destroy_nodes'
33 33
     BootactionReport = 'bootaction_report'
34
+    RelabelNodes = 'relabel_nodes'
34 35
 
35 36
     # OOB driver actions
36 37
     ValidateOobServices = 'validate_oob_services'
@@ -64,14 +65,17 @@ class OrchestratorAction(BaseDrydockEnum):
64 65
     ConfigurePortProvisioning = 'config_port_provisioning'
65 66
     ConfigurePortProduction = 'config_port_production'
66 67
 
68
+    # Kubernetes driver actions
69
+    RelabelNode = 'relabel_node'
70
+
67 71
     ALL = (Noop, ValidateDesign, VerifySite, PrepareSite, VerifyNodes,
68 72
            PrepareNodes, DeployNodes, BootactionReport, DestroyNodes,
69
-           ConfigNodePxe, SetNodeBoot, PowerOffNode, PowerOnNode,
70
-           PowerCycleNode, InterrogateOob, CreateNetworkTemplate,
71
-           CreateStorageTemplate, CreateBootMedia, PrepareHardwareConfig,
72
-           ConfigureHardware, InterrogateNode, ApplyNodeNetworking,
73
-           ApplyNodeStorage, ApplyNodePlatform, DeployNode, DestroyNode,
74
-           ConfigureNodeProvisioner)
73
+           RelabelNodes, ConfigNodePxe, SetNodeBoot, PowerOffNode,
74
+           PowerOnNode, PowerCycleNode, InterrogateOob, RelabelNode,
75
+           CreateNetworkTemplate, CreateStorageTemplate, CreateBootMedia,
76
+           PrepareHardwareConfig, ConfigureHardware, InterrogateNode,
77
+           ApplyNodeNetworking, ApplyNodeStorage, ApplyNodePlatform,
78
+           DeployNode, DestroyNode, ConfigureNodeProvisioner)
75 79
 
76 80
 
77 81
 class OrchestratorActionField(fields.BaseEnumField):

+ 11
- 0
python/drydock_provisioner/objects/node.py View File

@@ -326,6 +326,17 @@ class BaremetalNode(drydock_provisioner.objects.hostprofile.HostProfile):
326 326
                 alias)
327 327
             return alias
328 328
 
329
+    def get_node_labels(self):
330
+        """Get node labels.
331
+        """
332
+
333
+        labels_dict = {}
334
+        for k, v in self.owner_data.items():
335
+            labels_dict[k] = v
336
+        self.logger.debug("node labels data : %s." % str(labels_dict))
337
+        # TODO: Generate node labels
338
+
339
+        return labels_dict
329 340
 
330 341
 @base.DrydockObjectRegistry.register
331 342
 class BaremetalNodeList(base.DrydockObjectListBase, base.DrydockObject):

+ 60
- 0
python/drydock_provisioner/orchestrator/actions/orchestrator.py View File

@@ -1035,6 +1035,66 @@ class DeployNodes(BaseAction):
1035 1035
         return
1036 1036
 
1037 1037
 
1038
+class RelabelNodes(BaseAction):
1039
+    """Action to relabel a node"""
1040
+
1041
+    def start(self):
1042
+        """Start executing this action."""
1043
+        self.task.set_status(hd_fields.TaskStatus.Running)
1044
+        self.task.save()
1045
+
1046
+        kubernetes_driver = self.orchestrator.enabled_drivers['kubernetes']
1047
+
1048
+        if kubernetes_driver is None:
1049
+            self.task.set_status(hd_fields.TaskStatus.Complete)
1050
+            self.task.add_status_msg(
1051
+                msg="No kubernetes driver is enabled, ending task.",
1052
+                error=True,
1053
+                ctx=str(self.task.get_id()),
1054
+                ctx_type='task')
1055
+            self.task.result.set_message("No KubernetesDriver enabled.")
1056
+            self.task.result.set_reason("Bad Configuration.")
1057
+            self.task.failure()
1058
+            self.task.save()
1059
+            return
1060
+
1061
+        target_nodes = self.orchestrator.get_target_nodes(self.task)
1062
+
1063
+        if not target_nodes:
1064
+            self.task.add_status_msg(
1065
+                msg="No nodes in scope, nothing to relabel.",
1066
+                error=False,
1067
+                ctx='NA',
1068
+                ctx_type='NA')
1069
+            self.task.success()
1070
+            self.task.set_status(hd_fields.TaskStatus.Complete)
1071
+            self.task.save()
1072
+            return
1073
+
1074
+        nf = self.orchestrator.create_nodefilter_from_nodelist(target_nodes)
1075
+
1076
+        relabel_node_task = self.orchestrator.create_task(
1077
+            design_ref=self.task.design_ref,
1078
+            action=hd_fields.OrchestratorAction.RelabelNode,
1079
+            node_filter=nf)
1080
+        self.task.register_subtask(relabel_node_task)
1081
+
1082
+        self.logger.info(
1083
+            "Starting kubernetes driver task %s to relabel nodes." %
1084
+            (relabel_node_task.get_id()))
1085
+        kubernetes_driver.execute_task(relabel_node_task.get_id())
1086
+
1087
+        relabel_node_task = self.state_manager.get_task(
1088
+            relabel_node_task.get_id())
1089
+
1090
+        self.task.bubble_results(
1091
+            action_filter=hd_fields.OrchestratorAction.RelabelNode)
1092
+        self.task.align_result()
1093
+
1094
+        self.task.set_status(hd_fields.TaskStatus.Complete)
1095
+        self.task.save()
1096
+
1097
+
1038 1098
 class BootactionReport(BaseAction):
1039 1099
     """Wait for nodes to report status of boot action."""
1040 1100
 

+ 11
- 0
python/drydock_provisioner/orchestrator/orchestrator.py View File

@@ -33,6 +33,7 @@ from .actions.orchestrator import PrepareSite
33 33
 from .actions.orchestrator import VerifyNodes
34 34
 from .actions.orchestrator import PrepareNodes
35 35
 from .actions.orchestrator import DeployNodes
36
+from .actions.orchestrator import RelabelNodes
36 37
 from .actions.orchestrator import DestroyNodes
37 38
 from .validations.validator import Validator
38 39
 
@@ -102,6 +103,15 @@ class Orchestrator(object):
102 103
                     self.enabled_drivers['network'] = network_driver_class(
103 104
                         state_manager=state_manager, orchestrator=self)
104 105
 
106
+            kubernetes_driver_name = enabled_drivers.kubernetes_driver
107
+            if kubernetes_driver_name is not None:
108
+                m, c = kubernetes_driver_name.rsplit('.', 1)
109
+                kubernetes_driver_class = getattr(
110
+                    importlib.import_module(m), c, None)
111
+                if kubernetes_driver_class is not None:
112
+                    self.enabled_drivers['kubernetes'] = kubernetes_driver_class(
113
+                        state_manager=state_manager, orchestrator=self)
114
+
105 115
     def watch_for_tasks(self):
106 116
         """Start polling the database watching for Queued tasks to execute."""
107 117
         orch_task_actions = {
@@ -112,6 +122,7 @@ class Orchestrator(object):
112 122
             hd_fields.OrchestratorAction.VerifyNodes: VerifyNodes,
113 123
             hd_fields.OrchestratorAction.PrepareNodes: PrepareNodes,
114 124
             hd_fields.OrchestratorAction.DeployNodes: DeployNodes,
125
+            hd_fields.OrchestratorAction.RelabelNodes: RelabelNodes,
115 126
             hd_fields.OrchestratorAction.DestroyNodes: DestroyNodes,
116 127
         }
117 128
 

+ 5
- 0
python/drydock_provisioner/orchestrator/readme.md View File

@@ -111,6 +111,11 @@ success
111 111
 
112 112
 Destroy current node configuration and rebootstrap from scratch
113 113
 
114
+### RelabelNode ###
115
+
116
+Relabel current Kubernetes cluster node through Kubernetes
117
+provisioner.
118
+
114 119
 ## Integration with Drivers ##
115 120
 
116 121
 Based on the requested task and the current known state of a node

+ 6
- 0
python/drydock_provisioner/policy.py View File

@@ -95,6 +95,12 @@ class DrydockPolicy(object):
95 95
                                          'path': '/api/v1.0/tasks',
96 96
                                          'method': 'POST'
97 97
                                      }]),
98
+        policy.DocumentedRuleDefault('physical_provisioner:relabel_nodes',
99
+                                     'role:admin', 'Create relabel_nodes task',
100
+                                     [{
101
+                                         'path': '/api/v1.0/tasks',
102
+                                         'method': 'POST'
103
+                                     }]),
98 104
         policy.DocumentedRuleDefault(
99 105
             'physical_provisioner:read_build_data', 'role:admin',
100 106
             'Read build data for a node',

+ 194
- 0
python/tests/unit/test_k8sdriver_promenade_client.py View File

@@ -0,0 +1,194 @@
1
+# Copyright 2018 AT&T Intellectual Property.  All other rights reserved.
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain a copy of the License at
6
+#
7
+#     http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+# See the License for the specific language governing permissions and
13
+# limitations under the License.
14
+from unittest import mock
15
+from urllib.parse import urlparse
16
+
17
+import pytest
18
+import responses
19
+
20
+import drydock_provisioner.error as errors
21
+from drydock_provisioner.drivers.kubernetes.promenade_driver.promenade_client \
22
+    import PromenadeSession, PromenadeClient
23
+
24
+PROM_URL = urlparse('http://promhost:80/api/v1.0')
25
+PROM_HOST = 'promhost'
26
+
27
+
28
+@mock.patch(
29
+    'drydock_provisioner.drivers.kubernetes'
30
+    '.promenade_driver.promenade_client'
31
+    '.PromenadeSession._get_prom_url',
32
+    return_value=PROM_URL)
33
+@mock.patch(
34
+    'drydock_provisioner.drivers.kubernetes'
35
+    '.promenade_driver.promenade_client'
36
+    '.PromenadeSession.set_auth',
37
+    return_value=None)
38
+@responses.activate
39
+def test_put(patch1, patch2):
40
+    """
41
+    Test put functionality
42
+    """
43
+    responses.add(
44
+        responses.PUT,
45
+        'http://promhost:80/api/v1.0/node-label/n1',
46
+        body='{"key1":"label1"}',
47
+        status=200)
48
+
49
+    prom_session = PromenadeSession()
50
+    result = prom_session.put('v1.0/node-label/n1',
51
+                              body='{"key1":"label1"}',
52
+                              timeout=(60, 60))
53
+
54
+    assert PROM_HOST == prom_session.host
55
+    assert result.status_code == 200
56
+
57
+
58
+@mock.patch(
59
+    'drydock_provisioner.drivers.kubernetes'
60
+    '.promenade_driver.promenade_client'
61
+    '.PromenadeSession._get_prom_url',
62
+    return_value=PROM_URL)
63
+@mock.patch(
64
+    'drydock_provisioner.drivers.kubernetes'
65
+    '.promenade_driver.promenade_client'
66
+    '.PromenadeSession.set_auth',
67
+    return_value=None)
68
+@responses.activate
69
+def test_get(patch1, patch2):
70
+    """
71
+    Test get functionality
72
+    """
73
+    responses.add(
74
+        responses.GET,
75
+        'http://promhost:80/api/v1.0/node-label/n1',
76
+        status=200)
77
+
78
+    prom_session = PromenadeSession()
79
+    result = prom_session.get('v1.0/node-label/n1',
80
+                              timeout=(60, 60))
81
+
82
+    assert result.status_code == 200
83
+
84
+
85
+@mock.patch(
86
+    'drydock_provisioner.drivers.kubernetes'
87
+    '.promenade_driver.promenade_client'
88
+    '.PromenadeSession._get_prom_url',
89
+    return_value=PROM_URL)
90
+@mock.patch(
91
+    'drydock_provisioner.drivers.kubernetes'
92
+    '.promenade_driver.promenade_client'
93
+    '.PromenadeSession.set_auth',
94
+    return_value=None)
95
+@responses.activate
96
+def test_post(patch1, patch2):
97
+    """
98
+    Test post functionality
99
+    """
100
+    responses.add(
101
+        responses.POST,
102
+        'http://promhost:80/api/v1.0/node-label/n1',
103
+        body='{"key1":"label1"}',
104
+        status=200)
105
+
106
+    prom_session = PromenadeSession()
107
+    result = prom_session.post('v1.0/node-label/n1',
108
+                               body='{"key1":"label1"}',
109
+                               timeout=(60, 60))
110
+
111
+    assert PROM_HOST == prom_session.host
112
+    assert result.status_code == 200
113
+
114
+
115
+@mock.patch(
116
+    'drydock_provisioner.drivers.kubernetes'
117
+    '.promenade_driver.promenade_client'
118
+    '.PromenadeSession._get_prom_url',
119
+    return_value=PROM_URL)
120
+@mock.patch(
121
+    'drydock_provisioner.drivers.kubernetes'
122
+    '.promenade_driver.promenade_client'
123
+    '.PromenadeSession.set_auth',
124
+    return_value=None)
125
+@responses.activate
126
+def test_relabel_node(patch1, patch2):
127
+    """
128
+    Test relabel node call from Promenade
129
+    Client
130
+    """
131
+    responses.add(
132
+        responses.PUT,
133
+        'http://promhost:80/api/v1.0/node-labels/n1',
134
+        body='{"key1":"label1"}',
135
+        status=200)
136
+
137
+    prom_client = PromenadeClient()
138
+
139
+    result = prom_client.relabel_node('n1', {"key1": "label1"})
140
+
141
+    assert result == {"key1": "label1"}
142
+
143
+
144
+@mock.patch(
145
+    'drydock_provisioner.drivers.kubernetes'
146
+    '.promenade_driver.promenade_client'
147
+    '.PromenadeSession._get_prom_url',
148
+    return_value=PROM_URL)
149
+@mock.patch(
150
+    'drydock_provisioner.drivers.kubernetes'
151
+    '.promenade_driver.promenade_client'
152
+    '.PromenadeSession.set_auth',
153
+    return_value=None)
154
+@responses.activate
155
+def test_relabel_node_403_status(patch1, patch2):
156
+    """
157
+    Test relabel node with 403 resp status
158
+    """
159
+    responses.add(
160
+        responses.PUT,
161
+        'http://promhost:80/api/v1.0/node-labels/n1',
162
+        body='{"key1":"label1"}',
163
+        status=403)
164
+
165
+    prom_client = PromenadeClient()
166
+
167
+    with pytest.raises(errors.ClientForbiddenError):
168
+        prom_client.relabel_node('n1', {"key1": "label1"})
169
+
170
+@mock.patch(
171
+    'drydock_provisioner.drivers.kubernetes'
172
+    '.promenade_driver.promenade_client'
173
+    '.PromenadeSession._get_prom_url',
174
+    return_value=PROM_URL)
175
+@mock.patch(
176
+    'drydock_provisioner.drivers.kubernetes'
177
+    '.promenade_driver.promenade_client'
178
+    '.PromenadeSession.set_auth',
179
+    return_value=None)
180
+@responses.activate
181
+def test_relabel_node_401_status(patch1, patch2):
182
+    """
183
+    Test relabel node with 401 resp status
184
+    """
185
+    responses.add(
186
+        responses.PUT,
187
+        'http://promhost:80/api/v1.0/node-labels/n1',
188
+        body='{"key1":"label1"}',
189
+        status=401)
190
+
191
+    prom_client = PromenadeClient()
192
+
193
+    with pytest.raises(errors.ClientUnauthorizedError):
194
+        prom_client.relabel_node('n1', {"key1": "label1"})

Loading…
Cancel
Save