123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656 |
- # -*- coding: utf-8 -*-
- # Import Python Libs
- from __future__ import absolute_import, print_function, unicode_literals
- import os
- import time
- import threading
- # Import Salt Libs
- import salt.utils.json
- import salt.utils.stringutils
- from salt.netapi.rest_tornado import saltnado
- from salt.utils.versions import StrictVersion
- # Import Salt Testing Libs
- from tests.unit.netapi.test_rest_tornado import SaltnadoTestCase
- from tests.support.helpers import flaky
- from tests.support.unit import skipIf
- # Import 3rd-party libs
- from salt.ext import six
- from salt.utils.zeromq import zmq, ZMQDefaultLoop as ZMQIOLoop
- HAS_ZMQ_IOLOOP = bool(zmq)
- class _SaltnadoIntegrationTestCase(SaltnadoTestCase): # pylint: disable=abstract-method
- @property
- def opts(self):
- return self.get_config('client_config', from_scratch=True)
- @property
- def mod_opts(self):
- return self.get_config('minion', from_scratch=True)
- @skipIf(HAS_ZMQ_IOLOOP is False, 'PyZMQ version must be >= 14.0.1 to run these tests.')
- @skipIf(StrictVersion(zmq.__version__) < StrictVersion('14.0.1'), 'PyZMQ must be >= 14.0.1 to run these tests.')
- class TestSaltAPIHandler(_SaltnadoIntegrationTestCase):
- def setUp(self):
- super(TestSaltAPIHandler, self).setUp()
- os.environ['ASYNC_TEST_TIMEOUT'] = '300'
- def get_app(self):
- urls = [('/', saltnado.SaltAPIHandler)]
- application = self.build_tornado_app(urls)
- application.event_listener = saltnado.EventListener({}, self.opts)
- self.application = application
- return application
- def test_root(self):
- '''
- Test the root path which returns the list of clients we support
- '''
- response = self.fetch('/',
- connect_timeout=30,
- request_timeout=30,
- )
- self.assertEqual(response.code, 200)
- response_obj = salt.utils.json.loads(response.body)
- self.assertEqual(sorted(response_obj['clients']),
- ['local', 'local_async', 'runner', 'runner_async'])
- self.assertEqual(response_obj['return'], 'Welcome')
- def test_post_no_auth(self):
- '''
- Test post with no auth token, should 401
- '''
- # get a token for this test
- low = [{'client': 'local',
- 'tgt': '*',
- 'fun': 'test.ping',
- }]
- response = self.fetch('/',
- method='POST',
- body=salt.utils.json.dumps(low),
- headers={'Content-Type': self.content_type_map['json']},
- follow_redirects=False,
- connect_timeout=30,
- request_timeout=30,
- )
- self.assertEqual(response.code, 302)
- self.assertEqual(response.headers['Location'], '/login')
- # Local client tests
- @skipIf(True, 'to be re-enabled when #23623 is merged')
- def test_simple_local_post(self):
- '''
- Test a basic API of /
- '''
- low = [{'client': 'local',
- 'tgt': '*',
- 'fun': 'test.ping',
- }]
- response = self.fetch('/',
- method='POST',
- body=salt.utils.json.dumps(low),
- headers={'Content-Type': self.content_type_map['json'],
- saltnado.AUTH_TOKEN_HEADER: self.token['token']},
- connect_timeout=30,
- request_timeout=30,
- )
- response_obj = salt.utils.json.loads(response.body)
- self.assertEqual(len(response_obj['return']), 1)
- # If --proxy is set, it will cause an extra minion_id to be in the
- # response. Since there's not a great way to know if the test
- # runner's proxy minion is running, and we're not testing proxy
- # minions here anyway, just remove it from the response.
- response_obj['return'][0].pop('proxytest', None)
- self.assertEqual(response_obj['return'][0], {'minion': True, 'sub_minion': True})
- def test_simple_local_post_no_tgt(self):
- '''
- POST job with invalid tgt
- '''
- low = [{'client': 'local',
- 'tgt': 'minion_we_dont_have',
- 'fun': 'test.ping',
- }]
- response = self.fetch('/',
- method='POST',
- body=salt.utils.json.dumps(low),
- headers={'Content-Type': self.content_type_map['json'],
- saltnado.AUTH_TOKEN_HEADER: self.token['token']},
- connect_timeout=30,
- request_timeout=30,
- )
- response_obj = salt.utils.json.loads(response.body)
- self.assertEqual(response_obj['return'], ["No minions matched the target. No command was sent, no jid was assigned."])
- # local client request body test
- @skipIf(True, 'Undetermined race condition in test. Temporarily disabled.')
- def test_simple_local_post_only_dictionary_request(self):
- '''
- Test a basic API of /
- '''
- low = {'client': 'local',
- 'tgt': '*',
- 'fun': 'test.ping',
- }
- response = self.fetch('/',
- method='POST',
- body=salt.utils.json.dumps(low),
- headers={'Content-Type': self.content_type_map['json'],
- saltnado.AUTH_TOKEN_HEADER: self.token['token']},
- connect_timeout=30,
- request_timeout=30,
- )
- response_obj = salt.utils.json.loads(response.body)
- self.assertEqual(len(response_obj['return']), 1)
- # If --proxy is set, it will cause an extra minion_id to be in the
- # response. Since there's not a great way to know if the test
- # runner's proxy minion is running, and we're not testing proxy
- # minions here anyway, just remove it from the response.
- response_obj['return'][0].pop('proxytest', None)
- self.assertEqual(response_obj['return'][0], {'minion': True, 'sub_minion': True})
- def test_simple_local_post_invalid_request(self):
- '''
- Test a basic API of /
- '''
- low = ["invalid request"]
- response = self.fetch('/',
- method='POST',
- body=salt.utils.json.dumps(low),
- headers={'Content-Type': self.content_type_map['json'],
- saltnado.AUTH_TOKEN_HEADER: self.token['token']},
- connect_timeout=30,
- request_timeout=30,
- )
- self.assertEqual(response.code, 400)
- # local_async tests
- def test_simple_local_async_post(self):
- low = [{'client': 'local_async',
- 'tgt': '*',
- 'fun': 'test.ping',
- }]
- response = self.fetch('/',
- method='POST',
- body=salt.utils.json.dumps(low),
- headers={'Content-Type': self.content_type_map['json'],
- saltnado.AUTH_TOKEN_HEADER: self.token['token']},
- )
- response_obj = salt.utils.json.loads(response.body)
- ret = response_obj['return']
- ret[0]['minions'] = sorted(ret[0]['minions'])
- try:
- # If --proxy is set, it will cause an extra minion_id to be in the
- # response. Since there's not a great way to know if the test
- # runner's proxy minion is running, and we're not testing proxy
- # minions here anyway, just remove it from the response.
- ret[0]['minions'].remove('proxytest')
- except ValueError:
- pass
- # TODO: verify pub function? Maybe look at how we test the publisher
- self.assertEqual(len(ret), 1)
- self.assertIn('jid', ret[0])
- self.assertEqual(ret[0]['minions'], sorted(['minion', 'sub_minion']))
- def test_multi_local_async_post(self):
- low = [{'client': 'local_async',
- 'tgt': '*',
- 'fun': 'test.ping',
- },
- {'client': 'local_async',
- 'tgt': '*',
- 'fun': 'test.ping',
- }]
- response = self.fetch('/',
- method='POST',
- body=salt.utils.json.dumps(low),
- headers={'Content-Type': self.content_type_map['json'],
- saltnado.AUTH_TOKEN_HEADER: self.token['token']},
- )
- response_obj = salt.utils.json.loads(response.body)
- ret = response_obj['return']
- ret[0]['minions'] = sorted(ret[0]['minions'])
- ret[1]['minions'] = sorted(ret[1]['minions'])
- try:
- # If --proxy is set, it will cause an extra minion_id to be in the
- # response. Since there's not a great way to know if the test
- # runner's proxy minion is running, and we're not testing proxy
- # minions here anyway, just remove it from the response.
- ret[0]['minions'].remove('proxytest')
- ret[1]['minions'].remove('proxytest')
- except ValueError:
- pass
- self.assertEqual(len(ret), 2)
- self.assertIn('jid', ret[0])
- self.assertIn('jid', ret[1])
- self.assertEqual(ret[0]['minions'], sorted(['minion', 'sub_minion']))
- self.assertEqual(ret[1]['minions'], sorted(['minion', 'sub_minion']))
- def test_multi_local_async_post_multitoken(self):
- low = [{'client': 'local_async',
- 'tgt': '*',
- 'fun': 'test.ping',
- },
- {'client': 'local_async',
- 'tgt': '*',
- 'fun': 'test.ping',
- 'token': self.token['token'], # send a different (but still valid token)
- },
- {'client': 'local_async',
- 'tgt': '*',
- 'fun': 'test.ping',
- 'token': 'BAD_TOKEN', # send a bad token
- },
- ]
- response = self.fetch('/',
- method='POST',
- body=salt.utils.json.dumps(low),
- headers={'Content-Type': self.content_type_map['json'],
- saltnado.AUTH_TOKEN_HEADER: self.token['token']},
- )
- response_obj = salt.utils.json.loads(response.body)
- ret = response_obj['return']
- ret[0]['minions'] = sorted(ret[0]['minions'])
- ret[1]['minions'] = sorted(ret[1]['minions'])
- try:
- # If --proxy is set, it will cause an extra minion_id to be in the
- # response. Since there's not a great way to know if the test
- # runner's proxy minion is running, and we're not testing proxy
- # minions here anyway, just remove it from the response.
- ret[0]['minions'].remove('proxytest')
- ret[1]['minions'].remove('proxytest')
- except ValueError:
- pass
- self.assertEqual(len(ret), 3) # make sure we got 3 responses
- self.assertIn('jid', ret[0]) # the first 2 are regular returns
- self.assertIn('jid', ret[1])
- self.assertIn('Failed to authenticate', ret[2]) # bad auth
- self.assertEqual(ret[0]['minions'], sorted(['minion', 'sub_minion']))
- self.assertEqual(ret[1]['minions'], sorted(['minion', 'sub_minion']))
- def test_simple_local_async_post_no_tgt(self):
- low = [{'client': 'local_async',
- 'tgt': 'minion_we_dont_have',
- 'fun': 'test.ping',
- }]
- response = self.fetch('/',
- method='POST',
- body=salt.utils.json.dumps(low),
- headers={'Content-Type': self.content_type_map['json'],
- saltnado.AUTH_TOKEN_HEADER: self.token['token']},
- )
- response_obj = salt.utils.json.loads(response.body)
- self.assertEqual(response_obj['return'], [{}])
- @skipIf(True, 'Undetermined race condition in test. Temporarily disabled.')
- def test_simple_local_post_only_dictionary_request_with_order_masters(self):
- '''
- Test a basic API of /
- '''
- low = {'client': 'local',
- 'tgt': '*',
- 'fun': 'test.ping',
- }
- self.application.opts['order_masters'] = True
- self.application.opts['syndic_wait'] = 5
- response = self.fetch('/',
- method='POST',
- body=salt.utils.json.dumps(low),
- headers={'Content-Type': self.content_type_map['json'],
- saltnado.AUTH_TOKEN_HEADER: self.token['token']},
- connect_timeout=30,
- request_timeout=30,
- )
- response_obj = salt.utils.json.loads(response.body)
- self.application.opts['order_masters'] = []
- self.application.opts['syndic_wait'] = 5
- # If --proxy is set, it will cause an extra minion_id to be in the
- # response. Since there's not a great way to know if the test runner's
- # proxy minion is running, and we're not testing proxy minions here
- # anyway, just remove it from the response.
- response_obj[0]['return'].pop('proxytest', None)
- self.assertEqual(response_obj['return'], [{'minion': True, 'sub_minion': True}])
- # runner tests
- def test_simple_local_runner_post(self):
- low = [{'client': 'runner',
- 'fun': 'manage.up',
- }]
- response = self.fetch('/',
- method='POST',
- body=salt.utils.json.dumps(low),
- headers={'Content-Type': self.content_type_map['json'],
- saltnado.AUTH_TOKEN_HEADER: self.token['token']},
- connect_timeout=30,
- request_timeout=300,
- )
- response_obj = salt.utils.json.loads(response.body)
- self.assertEqual(len(response_obj['return']), 1)
- try:
- # If --proxy is set, it will cause an extra minion_id to be in the
- # response. Since there's not a great way to know if the test
- # runner's proxy minion is running, and we're not testing proxy
- # minions here anyway, just remove it from the response.
- response_obj['return'][0].remove('proxytest')
- except ValueError:
- pass
- self.assertEqual(sorted(response_obj['return'][0]), sorted(['minion', 'sub_minion']))
- # runner_async tests
- def test_simple_local_runner_async_post(self):
- low = [{'client': 'runner_async',
- 'fun': 'manage.up',
- }]
- response = self.fetch('/',
- method='POST',
- body=salt.utils.json.dumps(low),
- headers={'Content-Type': self.content_type_map['json'],
- saltnado.AUTH_TOKEN_HEADER: self.token['token']},
- connect_timeout=10,
- request_timeout=10,
- )
- response_obj = salt.utils.json.loads(response.body)
- self.assertIn('return', response_obj)
- self.assertEqual(1, len(response_obj['return']))
- self.assertIn('jid', response_obj['return'][0])
- self.assertIn('tag', response_obj['return'][0])
- @flaky
- @skipIf(HAS_ZMQ_IOLOOP is False, 'PyZMQ version must be >= 14.0.1 to run these tests.')
- class TestMinionSaltAPIHandler(_SaltnadoIntegrationTestCase):
- def get_app(self):
- urls = [(r"/minions/(.*)", saltnado.MinionSaltAPIHandler),
- (r"/minions", saltnado.MinionSaltAPIHandler),
- ]
- application = self.build_tornado_app(urls)
- application.event_listener = saltnado.EventListener({}, self.opts)
- return application
- @skipIf(True, 'issue #34753')
- def test_get_no_mid(self):
- response = self.fetch('/minions',
- method='GET',
- headers={saltnado.AUTH_TOKEN_HEADER: self.token['token']},
- follow_redirects=False,
- )
- response_obj = salt.utils.json.loads(response.body)
- self.assertEqual(len(response_obj['return']), 1)
- # one per minion
- self.assertEqual(len(response_obj['return'][0]), 2)
- # check a single grain
- for minion_id, grains in six.iteritems(response_obj['return'][0]):
- self.assertEqual(minion_id, grains['id'])
- @skipIf(True, 'to be re-enabled when #23623 is merged')
- def test_get(self):
- response = self.fetch('/minions/minion',
- method='GET',
- headers={saltnado.AUTH_TOKEN_HEADER: self.token['token']},
- follow_redirects=False,
- )
- response_obj = salt.utils.json.loads(response.body)
- self.assertEqual(len(response_obj['return']), 1)
- self.assertEqual(len(response_obj['return'][0]), 1)
- # check a single grain
- self.assertEqual(response_obj['return'][0]['minion']['id'], 'minion')
- def test_post(self):
- low = [{'tgt': '*minion',
- 'fun': 'test.ping',
- }]
- response = self.fetch('/minions',
- method='POST',
- body=salt.utils.json.dumps(low),
- headers={'Content-Type': self.content_type_map['json'],
- saltnado.AUTH_TOKEN_HEADER: self.token['token']},
- )
- response_obj = salt.utils.json.loads(response.body)
- ret = response_obj['return']
- ret[0]['minions'] = sorted(ret[0]['minions'])
- # TODO: verify pub function? Maybe look at how we test the publisher
- self.assertEqual(len(ret), 1)
- self.assertIn('jid', ret[0])
- self.assertEqual(ret[0]['minions'], sorted(['minion', 'sub_minion']))
- def test_post_with_client(self):
- # get a token for this test
- low = [{'client': 'local_async',
- 'tgt': '*minion',
- 'fun': 'test.ping',
- }]
- response = self.fetch('/minions',
- method='POST',
- body=salt.utils.json.dumps(low),
- headers={'Content-Type': self.content_type_map['json'],
- saltnado.AUTH_TOKEN_HEADER: self.token['token']},
- )
- response_obj = salt.utils.json.loads(response.body)
- ret = response_obj['return']
- ret[0]['minions'] = sorted(ret[0]['minions'])
- # TODO: verify pub function? Maybe look at how we test the publisher
- self.assertEqual(len(ret), 1)
- self.assertIn('jid', ret[0])
- self.assertEqual(ret[0]['minions'], sorted(['minion', 'sub_minion']))
- def test_post_with_incorrect_client(self):
- '''
- The /minions endpoint is asynchronous only, so if you try something else
- make sure you get an error
- '''
- # get a token for this test
- low = [{'client': 'local',
- 'tgt': '*',
- 'fun': 'test.ping',
- }]
- response = self.fetch('/minions',
- method='POST',
- body=salt.utils.json.dumps(low),
- headers={'Content-Type': self.content_type_map['json'],
- saltnado.AUTH_TOKEN_HEADER: self.token['token']},
- )
- self.assertEqual(response.code, 400)
- @skipIf(HAS_ZMQ_IOLOOP is False, 'PyZMQ version must be >= 14.0.1 to run these tests.')
- class TestJobsSaltAPIHandler(_SaltnadoIntegrationTestCase):
- def get_app(self):
- urls = [(r"/jobs/(.*)", saltnado.JobsSaltAPIHandler),
- (r"/jobs", saltnado.JobsSaltAPIHandler),
- ]
- application = self.build_tornado_app(urls)
- application.event_listener = saltnado.EventListener({}, self.opts)
- return application
- @skipIf(True, 'to be re-enabled when #23623 is merged')
- def test_get(self):
- # test with no JID
- self.http_client.fetch(self.get_url('/jobs'),
- self.stop,
- method='GET',
- headers={saltnado.AUTH_TOKEN_HEADER: self.token['token']},
- follow_redirects=False,
- )
- response = self.wait(timeout=30)
- response_obj = salt.utils.json.loads(response.body)['return'][0]
- try:
- for jid, ret in six.iteritems(response_obj):
- self.assertIn('Function', ret)
- self.assertIn('Target', ret)
- self.assertIn('Target-type', ret)
- self.assertIn('User', ret)
- self.assertIn('StartTime', ret)
- self.assertIn('Arguments', ret)
- except AttributeError as attribute_error:
- print(salt.utils.json.loads(response.body))
- raise
- # test with a specific JID passed in
- jid = next(six.iterkeys(response_obj))
- self.http_client.fetch(self.get_url('/jobs/{0}'.format(jid)),
- self.stop,
- method='GET',
- headers={saltnado.AUTH_TOKEN_HEADER: self.token['token']},
- follow_redirects=False,
- )
- response = self.wait(timeout=30)
- response_obj = salt.utils.json.loads(response.body)['return'][0]
- self.assertIn('Function', response_obj)
- self.assertIn('Target', response_obj)
- self.assertIn('Target-type', response_obj)
- self.assertIn('User', response_obj)
- self.assertIn('StartTime', response_obj)
- self.assertIn('Arguments', response_obj)
- self.assertIn('Result', response_obj)
- # TODO: run all the same tests from the root handler, but for now since they are
- # the same code, we'll just sanity check
- @skipIf(HAS_ZMQ_IOLOOP is False, 'PyZMQ version must be >= 14.0.1 to run these tests.')
- class TestRunSaltAPIHandler(_SaltnadoIntegrationTestCase):
- def get_app(self):
- urls = [("/run", saltnado.RunSaltAPIHandler),
- ]
- application = self.build_tornado_app(urls)
- application.event_listener = saltnado.EventListener({}, self.opts)
- return application
- @skipIf(True, 'to be re-enabled when #23623 is merged')
- def test_get(self):
- low = [{'client': 'local',
- 'tgt': '*',
- 'fun': 'test.ping',
- }]
- response = self.fetch('/run',
- method='POST',
- body=salt.utils.json.dumps(low),
- headers={'Content-Type': self.content_type_map['json'],
- saltnado.AUTH_TOKEN_HEADER: self.token['token']},
- )
- response_obj = salt.utils.json.loads(response.body)
- self.assertEqual(response_obj['return'], [{'minion': True, 'sub_minion': True}])
- @skipIf(HAS_ZMQ_IOLOOP is False, 'PyZMQ version must be >= 14.0.1 to run these tests.')
- class TestEventsSaltAPIHandler(_SaltnadoIntegrationTestCase):
- def get_app(self):
- urls = [(r"/events", saltnado.EventsSaltAPIHandler),
- ]
- application = self.build_tornado_app(urls)
- application.event_listener = saltnado.EventListener({}, self.opts)
- # store a reference, for magic later!
- self.application = application
- self.events_to_fire = 0
- return application
- def test_get(self):
- self.events_to_fire = 5
- response = self.fetch('/events',
- headers={saltnado.AUTH_TOKEN_HEADER: self.token['token']},
- streaming_callback=self.on_event,
- )
- def _stop(self):
- self.stop()
- def on_event(self, event):
- if six.PY3:
- event = event.decode('utf-8')
- if self.events_to_fire > 0:
- self.application.event_listener.event.fire_event({
- 'foo': 'bar',
- 'baz': 'qux',
- }, 'salt/netapi/test')
- self.events_to_fire -= 1
- # once we've fired all the events, lets call it a day
- else:
- # wait so that we can ensure that the next future is ready to go
- # to make sure we don't explode if the next one is ready
- ZMQIOLoop.current().add_timeout(time.time() + 0.5, self._stop)
- event = event.strip()
- # if we got a retry, just continue
- if event != 'retry: 400':
- tag, data = event.splitlines()
- self.assertTrue(tag.startswith('tag: '))
- self.assertTrue(data.startswith('data: '))
- @skipIf(HAS_ZMQ_IOLOOP is False, 'PyZMQ version must be >= 14.0.1 to run these tests.')
- class TestWebhookSaltAPIHandler(_SaltnadoIntegrationTestCase):
- def get_app(self):
- urls = [(r"/hook(/.*)?", saltnado.WebhookSaltAPIHandler),
- ]
- application = self.build_tornado_app(urls)
- self.application = application
- application.event_listener = saltnado.EventListener({}, self.opts)
- return application
- @skipIf(True, 'Skipping until we can devote more resources to debugging this test.')
- def test_post(self):
- self._future_resolved = threading.Event()
- try:
- def verify_event(future):
- '''
- Notify the threading event that the future is resolved
- '''
- self._future_resolved.set()
- self._finished = False # TODO: remove after some cleanup of the event listener
- # get an event future
- future = self.application.event_listener.get_event(self,
- tag='salt/netapi/hook',
- callback=verify_event)
- # fire the event
- response = self.fetch('/hook',
- method='POST',
- body='foo=bar',
- headers={saltnado.AUTH_TOKEN_HEADER: self.token['token']},
- )
- response_obj = salt.utils.json.loads(response.body)
- self.assertTrue(response_obj['success'])
- resolve_future_timeout = 60
- self._future_resolved.wait(resolve_future_timeout)
- try:
- event = future.result()
- except Exception as exc: # pylint: disable=broad-except
- self.fail('Failed to resolve future under {} secs: {}'.format(resolve_future_timeout, exc))
- self.assertEqual(event['tag'], 'salt/netapi/hook')
- self.assertIn('headers', event['data'])
- self.assertEqual(
- event['data']['post'],
- {'foo': salt.utils.stringutils.to_bytes('bar')}
- )
- finally:
- self._future_resolved.clear()
- del self._future_resolved
|