1# Licensed under the Apache License, Version 2.0 (the "License");
2# you may not use this file except in compliance with the License.
3# You may obtain a copy of the License at
4#
5#    http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS,
9# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
10# implied.
11# See the License for the specific language governing permissions and
12# limitations under the License.
13
14from unittest import mock
15
16import six
17import swiftclient.client
18import testscenarios
19import testtools
20from testtools import matchers
21
22from heatclient.common import deployment_utils
23from heatclient import exc
24from heatclient.v1 import software_configs
25
26
27load_tests = testscenarios.load_tests_apply_scenarios
28
29
30def mock_sc(group=None, config=None, options=None,
31            inputs=None, outputs=None):
32    return software_configs.SoftwareConfig(None, {
33        'group': group,
34        'config': config,
35        'options': options or {},
36        'inputs': inputs or [],
37        'outputs': outputs or [],
38    }, True)
39
40
41class DerivedConfigTest(testtools.TestCase):
42
43    scenarios = [
44        ('defaults', dict(
45            action='UPDATE',
46            source=mock_sc(),
47            name='s1',
48            input_values=None,
49            server_id='1234',
50            signal_transport='NO_SIGNAL',
51            signal_id=None,
52            result={
53                'config': '',
54                'group': 'Heat::Ungrouped',
55                'inputs': [{
56                    'description': 'ID of the server being deployed to',
57                    'name': 'deploy_server_id',
58                    'type': 'String',
59                    'value': '1234'
60                }, {
61                    'description': 'Name of the current action '
62                    'being deployed',
63                    'name': 'deploy_action',
64                    'type': 'String',
65                    'value': 'UPDATE'
66                }, {
67                    'description': 'How the server should signal to '
68                    'heat with the deployment output values.',
69                    'name': 'deploy_signal_transport',
70                    'type': 'String',
71                    'value': 'NO_SIGNAL'}],
72                'name': 's1',
73                'options': {},
74                'outputs': []})),
75        ('defaults_empty', dict(
76            action='UPDATE',
77            source={},
78            name='s1',
79            input_values=None,
80            server_id='1234',
81            signal_transport='NO_SIGNAL',
82            signal_id=None,
83            result={
84                'config': '',
85                'group': 'Heat::Ungrouped',
86                'inputs': [{
87                    'description': 'ID of the server being deployed to',
88                    'name': 'deploy_server_id',
89                    'type': 'String',
90                    'value': '1234'
91                }, {
92                    'description': 'Name of the current action '
93                    'being deployed',
94                    'name': 'deploy_action',
95                    'type': 'String',
96                    'value': 'UPDATE'
97                }, {
98                    'description': 'How the server should signal to '
99                    'heat with the deployment output values.',
100                    'name': 'deploy_signal_transport',
101                    'type': 'String',
102                    'value': 'NO_SIGNAL'}],
103                'name': 's1',
104                'options': {},
105                'outputs': []})),
106
107        ('config_values', dict(
108            action='UPDATE',
109            source=mock_sc(
110                group='puppet',
111                config='do the foo',
112                inputs=[
113                    {'name': 'one', 'default': '1'},
114                    {'name': 'two'}],
115                options={'option1': 'value'},
116                outputs=[
117                    {'name': 'output1'},
118                    {'name': 'output2'}],
119            ),
120            name='s2',
121            input_values={'one': 'foo', 'two': 'bar', 'three': 'baz'},
122            server_id='1234',
123            signal_transport='NO_SIGNAL',
124            signal_id=None,
125            result={
126                'config': 'do the foo',
127                'group': 'puppet',
128                'inputs': [{
129                    'name': 'one',
130                    'default': '1',
131                    'value': 'foo'
132                }, {
133                    'name': 'two',
134                    'value': 'bar'
135                }, {
136                    'name': 'three',
137                    'type': 'String',
138                    'value': 'baz'
139                }, {
140                    'description': 'ID of the server being deployed to',
141                    'name': 'deploy_server_id',
142                    'type': 'String',
143                    'value': '1234'
144                }, {
145                    'description': 'Name of the current action '
146                    'being deployed',
147                    'name': 'deploy_action',
148                    'type': 'String',
149                    'value': 'UPDATE'
150                }, {
151                    'description': 'How the server should signal to '
152                    'heat with the deployment output values.',
153                    'name': 'deploy_signal_transport',
154                    'type': 'String',
155                    'value': 'NO_SIGNAL'
156                }],
157                'name': 's2',
158                'options': {'option1': 'value'},
159                'outputs': [
160                    {'name': 'output1'},
161                    {'name': 'output2'}]})),
162        ('temp_url', dict(
163            action='UPDATE',
164            source=mock_sc(),
165            name='s1',
166            input_values=None,
167            server_id='1234',
168            signal_transport='TEMP_URL_SIGNAL',
169            signal_id='http://192.0.2.1:8080/foo',
170            result={
171                'config': '',
172                'group': 'Heat::Ungrouped',
173                'inputs': [{
174                    'description': 'ID of the server being deployed to',
175                    'name': 'deploy_server_id',
176                    'type': 'String',
177                    'value': '1234'
178                }, {
179                    'description': 'Name of the current action '
180                    'being deployed',
181                    'name': 'deploy_action',
182                    'type': 'String',
183                    'value': 'UPDATE'
184                }, {
185                    'description': 'How the server should signal to '
186                    'heat with the deployment output values.',
187                    'name': 'deploy_signal_transport',
188                    'type': 'String',
189                    'value': 'TEMP_URL_SIGNAL'
190                }, {
191                    'description': 'ID of signal to use for signaling '
192                    'output values',
193                    'name': 'deploy_signal_id',
194                    'type': 'String',
195                    'value': 'http://192.0.2.1:8080/foo'
196                }, {
197                    'description': 'HTTP verb to use for signaling '
198                    'output values',
199                    'name': 'deploy_signal_verb',
200                    'type': 'String',
201                    'value': 'PUT'}],
202                'name': 's1',
203                'options': {},
204                'outputs': []})),
205        ('unsupported', dict(
206            action='UPDATE',
207            source=mock_sc(),
208            name='s1',
209            input_values=None,
210            server_id='1234',
211            signal_transport='ASDF',
212            signal_id=None,
213            result_error=exc.CommandError,
214            result_error_msg='Unsupported signal transport ASDF',
215            result=None)),
216    ]
217
218    def test_build_derived_config_params(self):
219        try:
220            self.assertEqual(
221                self.result,
222                deployment_utils.build_derived_config_params(
223                    action=self.action,
224                    source=self.source,
225                    name=self.name,
226                    input_values=self.input_values,
227                    server_id=self.server_id,
228                    signal_transport=self.signal_transport,
229                    signal_id=self.signal_id))
230        except Exception as e:
231            if not self.result_error:
232                raise e
233            self.assertIsInstance(e, self.result_error)
234            self.assertEqual(self.result_error_msg, six.text_type(e))
235
236
237class TempURLSignalTest(testtools.TestCase):
238
239    @mock.patch.object(swiftclient.client, 'Connection')
240    def test_create_swift_client(self, sc_conn):
241        auth = mock.MagicMock()
242        auth.get_token.return_value = '1234'
243        auth.get_endpoint.return_value = 'http://192.0.2.1:8080'
244
245        session = mock.MagicMock()
246
247        args = mock.MagicMock()
248        args.os_region_name = 'Region1'
249        args.os_project_name = 'project'
250        args.os_username = 'user'
251        args.os_cacert = None
252        args.insecure = True
253
254        sc_conn.return_value = mock.MagicMock()
255
256        sc = deployment_utils.create_swift_client(auth, session, args)
257
258        self.assertEqual(sc_conn.return_value, sc)
259
260        self.assertEqual(
261            mock.call(session),
262            auth.get_token.call_args)
263
264        self.assertEqual(
265            mock.call(
266                session,
267                service_type='object-store',
268                region_name='Region1'),
269            auth.get_endpoint.call_args)
270
271        self.assertEqual(
272            mock.call(
273                cacert=None,
274                insecure=True,
275                key=None,
276                tenant_name='project',
277                preauthtoken='1234',
278                authurl=None,
279                user='user',
280                preauthurl='http://192.0.2.1:8080',
281                auth_version='2.0'),
282            sc_conn.call_args)
283
284    def test_create_temp_url(self):
285        swift_client = mock.MagicMock()
286        swift_client.url = ("http://fake-host.com:8080/v1/AUTH_demo")
287        swift_client.head_account = mock.Mock(return_value={
288            'x-account-meta-temp-url-key': '123456'})
289        swift_client.post_account = mock.Mock()
290
291        uuid_pattern = ('[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB]'
292                        '[a-f0-9]{3}-[a-f0-9]{12}')
293        url = deployment_utils.create_temp_url(swift_client, 'bar', 60)
294        self.assertFalse(swift_client.post_account.called)
295        regexp = (r"http://fake-host.com:8080/v1/AUTH_demo/bar-%s"
296                  r"/%s\?temp_url_sig=[0-9a-f]{40}&"
297                  r"temp_url_expires=[0-9]{10}" % (uuid_pattern, uuid_pattern))
298        self.assertThat(url, matchers.MatchesRegex(regexp))
299
300        timeout = int(url.split('=')[-1])
301        self.assertTrue(timeout < 2147483647)
302
303    def test_get_temp_url_no_account_key(self):
304        swift_client = mock.MagicMock()
305        swift_client.url = ("http://fake-host.com:8080/v1/AUTH_demo")
306        head_account = {}
307
308        def post_account(data):
309            head_account.update(data)
310
311        swift_client.head_account = mock.Mock(return_value=head_account)
312        swift_client.post_account = post_account
313
314        self.assertNotIn('x-account-meta-temp-url-key', head_account)
315        deployment_utils.create_temp_url(swift_client, 'bar', 60, 'foo')
316        self.assertIn('x-account-meta-temp-url-key', head_account)
317
318    def test_build_signal_id_no_signal(self):
319        hc = mock.MagicMock()
320        args = mock.MagicMock()
321        args.signal_transport = 'NO_SIGNAL'
322        self.assertIsNone(deployment_utils.build_signal_id(hc, args))
323
324    def test_build_signal_id_no_client_auth(self):
325        hc = mock.MagicMock()
326        args = mock.MagicMock()
327        args.os_no_client_auth = True
328        args.signal_transport = 'TEMP_URL_SIGNAL'
329        e = self.assertRaises(exc.CommandError,
330                              deployment_utils.build_signal_id, hc, args)
331        self.assertEqual((
332            'Cannot use --os-no-client-auth, auth required to create '
333            'a Swift TempURL.'),
334            six.text_type(e))
335
336    @mock.patch.object(deployment_utils, 'create_temp_url')
337    @mock.patch.object(deployment_utils, 'create_swift_client')
338    def test_build_signal_id(self, csc, ctu):
339        hc = mock.MagicMock()
340        args = mock.MagicMock()
341        args.name = 'foo'
342        args.timeout = 60
343        args.os_no_client_auth = False
344        args.signal_transport = 'TEMP_URL_SIGNAL'
345        csc.return_value = mock.MagicMock()
346        temp_url = (
347            'http://fake-host.com:8080/v1/AUTH_demo/foo/'
348            'a81a74d5-c395-4269-9670-ddd0824fd696'
349            '?temp_url_sig=6a68371d602c7a14aaaa9e3b3a63b8b85bd9a503'
350            '&temp_url_expires=1425270977')
351        ctu.return_value = temp_url
352
353        self.assertEqual(
354            temp_url, deployment_utils.build_signal_id(hc, args))
355        self.assertEqual(
356            mock.call(hc.http_client.auth, hc.http_client.session, args),
357            csc.call_args)
358        self.assertEqual(
359            mock.call(csc.return_value, 'foo', 60),
360            ctu.call_args)
361