diff --git a/README.rst b/README.rst index 4f0b1074c..60447f3d5 100644 --- a/README.rst +++ b/README.rst @@ -142,7 +142,7 @@ of UUID's, ``base_url`` --- optional *ironic-discoverd* service URL and You can also use it from CLI:: - python -m ironic_discoverd.client --auth-token TOKEN UUID1 UUID2 + python -m ironic_discoverd.client --auth-token TOKEN introspect UUID .. note:: This CLI interface is not stable and may be changes without prior notice. @@ -153,26 +153,42 @@ API By default *ironic-discoverd* listens on ``0.0.0.0:5050``, this can be changed in configuration. Protocol is JSON over HTTP. -HTTP API consist of 2 endpoints: +HTTP API consist of these endpoints: -* ``POST /v1/discover`` initiate hardware discovery. Request body: JSON - list - of UUID's of nodes to discover. All power management configuration for these - nodes needs to be done prior to calling the endpoint. Requires X-Auth-Token - header with Keystone token for authentication. +* ``POST /v1/introspection/`` initiate hardware discovery for node + ````. All power management configuration for this node needs to be done + prior to calling the endpoint. - Nodes will be put into maintenance mode during discovery. It's up to caller - to put them back into use after discovery is done. - - .. note:: - Before version 0.2.0 this endpoint was not authenticated. Now it is, - but check for admin role is not implemented yet - see `bug #1391866`_. + Requires X-Auth-Token header with Keystone token for authentication. Response: * 202 - accepted discovery request * 400 - bad request + * 401, 403 - missing or invalid authentication * 404 - node cannot be found + Client library function: ``ironic_discoverd.client.introspect`` for node + ````. + +* ``GET /v1/introspection/`` get hardware discovery status. + + Requires X-Auth-Token header with Keystone token for authentication. + + Response: + + * 200 - OK + * 400 - bad request + * 401, 403 - missing or invalid authentication + * 404 - node cannot be found + + Response body: JSON dictionary with keys: + + * ``finished`` (boolean) whether discovery is finished + * ``error`` error string or ``null`` + + Client library function: ``ironic_discoverd.client.get_status``. + * ``POST /v1/continue`` internal endpoint for the discovery ramdisk to post back discovered data. Should not be used for anything other than implementing the ramdisk. Request body: JSON dictionary with keys: @@ -194,12 +210,6 @@ HTTP API consist of 2 endpoints: * 403 - node is not on discovery * 404 - node cannot be found or multiple nodes found - Successful response body is a JSON dictionary with keys: - - * ``node`` node as returned by Ironic - -.. _bug #1391866: https://bugs.launchpad.net/ironic-discoverd/+bug/1391866 - Release Notes ------------- @@ -213,10 +223,20 @@ See `1.0.0 release tracking page`_ for details. **API** +* New API ``GET /v1/introspection/`` and ``client.get_status`` for + getting discovery status. + + See `get-status-api blueprint`_ for details. + +* New API ``POST /v1/introspection/`` and ``client.introspect`` + is now used to initiate discovery, ``/v1/discover`` is deprecated. + + See `v1 API reform blueprint`_ for details. + * ``/v1/continue`` is now sync: * Errors are properly returned to the caller - * This call now returns value as a JSON dict + * This call now returns value as a JSON dict (currently empty) * Experimental support for updating IPMI credentials from within ramdisk. @@ -230,11 +250,6 @@ See `1.0.0 release tracking page`_ for details. * Add support for plugins that hook into data processing pipeline, see `plugin-architecture blueprint`_ for details. -* Add new API ``GET /v1/introspection/`` and ``client.get_status`` for - getting discovery status. - - See `get-status-api blueprint`_ for details. - **Configuration** * Cache nodes under discovery in a local SQLite database. Set ``database`` @@ -255,6 +270,7 @@ See `1.0.0 release tracking page`_ for details. .. _plugin-architecture blueprint: https://blueprints.launchpad.net/ironic-discoverd/+spec/plugin-architecture .. _get-status-api blueprint: https://blueprints.launchpad.net/ironic-discoverd/+spec/get-status-api .. _Kilo state machine blueprint: https://blueprints.launchpad.net/ironic-discoverd/+spec/kilo-state-machine +.. _v1 API reform blueprint: https://blueprints.launchpad.net/ironic-discoverd/+spec/v1-api-reform 0.2 Series ~~~~~~~~~~ diff --git a/functest/run.py b/functest/run.py index a4d3c079f..c7c46e7b2 100644 --- a/functest/run.py +++ b/functest/run.py @@ -98,7 +98,7 @@ class Test(base.NodeTest): def test_bmc(self): self.node.power_state = 'power off' - client.discover([self.uuid], auth_token='token') + client.introspect(self.uuid, auth_token='token') eventlet.greenthread.sleep(1) status = client.get_status(self.uuid, auth_token='token') diff --git a/ironic_discoverd/client.py b/ironic_discoverd/client.py index 112119314..2c915113b 100644 --- a/ironic_discoverd/client.py +++ b/ironic_discoverd/client.py @@ -11,6 +11,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import print_function + import argparse import json @@ -29,23 +31,20 @@ def _prepare(base_url, auth_token): return base_url, headers -def discover(uuids, base_url=_DEFAULT_URL, auth_token=''): - """Post node UUID's for discovery. +def introspect(uuid, base_url=_DEFAULT_URL, auth_token=''): + """Start introspection for a node. - :param uuids: list of UUID's. + :param uuid: node uuid :param base_url: *ironic-discoverd* URL in form: http://host:port[/ver], defaults to ``http://127.0.0.1:5050/v1``. :param auth_token: Keystone authentication token. - :raises: *requests* library HTTP errors. """ - if not all(isinstance(s, six.string_types) for s in uuids): - raise TypeError("Expected list of strings for uuids argument, got %s" % - uuids) + if not isinstance(uuid, six.string_types): + raise TypeError("Expected string for uuid argument, got %r" % uuid) base_url, headers = _prepare(base_url, auth_token) - headers['Content-Type'] = 'application/json' - res = requests.post(base_url + "/discover", - data=json.dumps(uuids), headers=headers) + res = requests.post("%s/introspection/%s" % (base_url, uuid), + headers=headers) res.raise_for_status() @@ -63,15 +62,35 @@ def get_status(uuid, base_url=_DEFAULT_URL, auth_token=''): raise TypeError("Expected string for uuid argument, got %r" % uuid) base_url, headers = _prepare(base_url, auth_token) - res = requests.get(base_url + "/introspection/%s" % uuid, headers=headers) + res = requests.get("%s/introspection/%s" % (base_url, uuid), + headers=headers) res.raise_for_status() return res.json() -if __name__ == '__main__': +def discover(uuids, base_url=_DEFAULT_URL, auth_token=''): + """Post node UUID's for discovery. + + DEPRECATED. Use introspect instead. + """ + if not all(isinstance(s, six.string_types) for s in uuids): + raise TypeError("Expected list of strings for uuids argument, got %s" % + uuids) + + base_url, headers = _prepare(base_url, auth_token) + headers['Content-Type'] = 'application/json' + res = requests.post(base_url + "/discover", + data=json.dumps(uuids), headers=headers) + res.raise_for_status() + + +if __name__ == '__main__': # pragma: no cover parser = argparse.ArgumentParser(description='Discover nodes.') - parser.add_argument('uuids', metavar='UUID', type=str, nargs='+', - help='node UUID\'s.') + parser.add_argument('cmd', metavar='cmd', + choices=['introspect', 'get_status'], + help='command: introspect or get_status.') + parser.add_argument('uuid', metavar='UUID', type=str, + help='node UUID.') parser.add_argument('--base-url', dest='base_url', action='store', default=_DEFAULT_URL, help='base URL, default to localhost.') @@ -79,4 +98,12 @@ if __name__ == '__main__': default='', help='Keystone token.') args = parser.parse_args() - discover(args.uuids, base_url=args.base_url, auth_token=args.auth_token) + func = globals()[args.cmd] + try: + res = func(uuid=args.uuid, base_url=args.base_url, + auth_token=args.auth_token) + except Exception as exc: + print('Error:', exc) + else: + if res: + print(res) diff --git a/ironic_discoverd/test/test_client.py b/ironic_discoverd/test/test_client.py index 5f7c0e7ec..3cc1cd01a 100644 --- a/ironic_discoverd/test/test_client.py +++ b/ironic_discoverd/test/test_client.py @@ -19,8 +19,37 @@ from ironic_discoverd import client @mock.patch.object(client.requests, 'post', autospec=True) -class TestDiscover(unittest.TestCase): +class TestIntrospect(unittest.TestCase): def test(self, mock_post): + client.introspect('uuid1', base_url="http://host:port", + auth_token="token") + mock_post.assert_called_once_with( + "http://host:port/v1/introspection/uuid1", + headers={'X-Auth-Token': 'token'} + ) + + def test_invalid_input(self, _): + self.assertRaises(TypeError, client.introspect, 42) + + def test_full_url(self, mock_post): + client.introspect('uuid1', base_url="http://host:port/v1/", + auth_token="token") + mock_post.assert_called_once_with( + "http://host:port/v1/introspection/uuid1", + headers={'X-Auth-Token': 'token'} + ) + + def test_default_url(self, mock_post): + client.introspect('uuid1', auth_token="token") + mock_post.assert_called_once_with( + "http://127.0.0.1:5050/v1/introspection/uuid1", + headers={'X-Auth-Token': 'token'} + ) + + +@mock.patch.object(client.requests, 'post', autospec=True) +class TestDiscover(unittest.TestCase): + def test_old_discover(self, mock_post): client.discover(['uuid1', 'uuid2'], base_url="http://host:port", auth_token="token") mock_post.assert_called_once_with( @@ -30,26 +59,6 @@ class TestDiscover(unittest.TestCase): 'X-Auth-Token': 'token'} ) - def test_full_url(self, mock_post): - client.discover(['uuid1', 'uuid2'], base_url="http://host:port/v1/", - auth_token="token") - mock_post.assert_called_once_with( - "http://host:port/v1/discover", - data='["uuid1", "uuid2"]', - headers={'Content-Type': 'application/json', - 'X-Auth-Token': 'token'} - ) - - def test_default_url(self, mock_post): - client.discover(['uuid1', 'uuid2'], - auth_token="token") - mock_post.assert_called_once_with( - "http://127.0.0.1:5050/v1/discover", - data='["uuid1", "uuid2"]', - headers={'Content-Type': 'application/json', - 'X-Auth-Token': 'token'} - ) - @mock.patch.object(client.requests, 'get', autospec=True) class TestGetStatus(unittest.TestCase):