diff --git a/ironicclient/tests/v1/test_node.py b/ironicclient/tests/v1/test_node.py new file mode 100644 index 000000000..5abe90901 --- /dev/null +++ b/ironicclient/tests/v1/test_node.py @@ -0,0 +1,101 @@ +# -*- encoding: utf-8 -*- +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +import testtools + +from ironicclient.tests import utils +import ironicclient.v1.node + +NODE = {'id': 123, + 'uuid': '66666666-7777-8888-9999-000000000000', + 'chassis_id': 42, + 'driver': 'fake', + 'driver_info': {'user': 'foo', 'password': 'bar'}, + 'properties': {'num_cpu': 4}, + 'extra': {}} + +CREATE_NODE = copy.deepcopy(NODE) +del CREATE_NODE['id'] +del CREATE_NODE['uuid'] + + +fixtures = { + '/v1/nodes': + { + 'GET': ( + {}, + {"nodes": [NODE]}, + ), + 'POST': ( + {}, + CREATE_NODE, + ), + }, + '/v1/nodes/%s' % NODE['uuid']: + { + 'GET': ( + {}, + NODE, + ), + 'DELETE': ( + {}, + None, + ), + }, +} + + +class NodeManagerTest(testtools.TestCase): + + def setUp(self): + super(NodeManagerTest, self).setUp() + self.api = utils.FakeAPI(fixtures) + self.mgr = ironicclient.v1.node.NodeManager(self.api) + + def test_node_list(self): + node = self.mgr.list() + expect = [ + ('GET', '/v1/nodes', {}, None), + ] + self.assertEqual(self.api.calls, expect) + self.assertEqual(len(node), 1) + + def test_node_show(self): + node = self.mgr.get(NODE['uuid']) + expect = [ + ('GET', '/v1/nodes/%s' % NODE['uuid'], {}, None), + ] + self.assertEqual(self.api.calls, expect) + self.assertEqual(node.uuid, NODE['uuid']) + + def test_create(self): + node = self.mgr.create(**CREATE_NODE) + expect = [ + ('POST', '/v1/nodes', {}, CREATE_NODE), + ] + self.assertEqual(self.api.calls, expect) + self.assertTrue(node) + + def test_delete(self): + node = self.mgr.delete(node_id=NODE['uuid']) + expect = [ + ('DELETE', '/v1/nodes/%s' % NODE['uuid'], {}, None), + ] + self.assertEqual(self.api.calls, expect) + self.assertTrue(node is None) diff --git a/ironicclient/v1/client.py b/ironicclient/v1/client.py index 427203612..5cab62c29 100644 --- a/ironicclient/v1/client.py +++ b/ironicclient/v1/client.py @@ -15,6 +15,7 @@ from ironicclient.common import http from ironicclient.v1 import chassis +from ironicclient.v1 import node from ironicclient.v1 import port @@ -32,4 +33,5 @@ class Client(http.HTTPClient): """Initialize a new client for the Ironic v1 API.""" super(Client, self).__init__(*args, **kwargs) self.chassis = chassis.ChassisManager(self) + self.node = node.NodeManager(self) self.port = port.PortManager(self) diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py new file mode 100644 index 000000000..8105d4e71 --- /dev/null +++ b/ironicclient/v1/node.py @@ -0,0 +1,56 @@ +# -*- encoding: utf-8 -*- +# +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from ironicclient.common import base +from ironicclient import exc + + +CREATION_ATTRIBUTES = ['chassis_id', 'driver', 'driver_info', 'extra', + 'node_id', 'properties'] + + +class Node(base.Resource): + def __repr__(self): + return "" % self._info + + +class NodeManager(base.Manager): + resource_class = Node + + @staticmethod + def _path(id=None): + return '/v1/nodes/%s' % id if id else '/v1/nodes' + + def list(self): + return self._list(self._path(), "nodes") + + def get(self, node_id): + try: + return self._list(self._path(node_id))[0] + except IndexError: + return None + + def create(self, **kwargs): + new = {} + for (key, value) in kwargs.items(): + if key in CREATION_ATTRIBUTES: + new[key] = value + else: + raise exc.InvalidAttribute() + return self._create(self._path(), new) + + def delete(self, node_id): + return self._delete(self._path(node_id)) diff --git a/ironicclient/v1/shell.py b/ironicclient/v1/shell.py index 2cbcd6775..7e965be2c 100644 --- a/ironicclient/v1/shell.py +++ b/ironicclient/v1/shell.py @@ -65,6 +65,71 @@ def do_chassis_delete(self, args): raise exc.CommandError('Chassis not found: %s' % args.chassis) +@utils.arg('node', metavar='', help="ID of node") +def do_node_show(self, args): + """Show a node.""" + try: + node = self.node.get(args.node) + except exc.HTTPNotFound: + raise exc.CommandError('Node not found: %s' % args.node) + else: + fields = ['uuid', 'instance_uuid', 'power_state', 'target_power_state', + 'provision_state', 'target_provision_state', 'driver', + 'driver_info', 'properties', 'extra', + 'created_at', 'updated_at', 'reservation'] + data = dict([(f, getattr(node, f, '')) for f in fields]) + utils.print_dict(data, wrap=72) + + +def do_node_list(self, args): + """List nodes.""" + nodes = self.node.list() + field_labels = ['UUID', 'Instance UUID', + 'Power State', 'Provisioning State'] + fields = ['uuid', 'instance_uuid', 'power_state', 'provision_state'] + utils.print_list(nodes, fields, field_labels, sortby=1) + + +@utils.arg('--driver', + metavar='', + help='Driver used to control the node. [REQUIRED]') +@utils.arg('--driver_info', + metavar='', + help='Key/value pairs used by the driver. ' + 'Can be specified multiple times.') +@utils.arg('--properties', + metavar='', + help='Key/value pairs describing the physical characteristics ' + 'of the node. This is exported to Nova and used by the ' + 'scheduler. Can be specified multiple times.') +@utils.arg('--extra', + metavar='', + help="Record arbitrary key/value metadata. " + "Can be specified multiple times.") +def do_node_create(self, args): + """Create a new node.""" + field_list = ['chassis_id', 'driver', 'driver_info', 'properties', 'extra'] + fields = dict((k, v) for (k, v) in vars(args).items() + if k in field_list and not (v is None)) + fields = utils.args_array_to_dict(fields, 'driver_info') + fields = utils.args_array_to_dict(fields, 'extra') + fields = utils.args_array_to_dict(fields, 'properties') + node = self.node.create(**fields) + + field_list.append('uuid') + data = dict([(f, getattr(node, f, '')) for f in field_list]) + utils.print_dict(data, wrap=72) + + +@utils.arg('node', metavar='', help="ID of node") +def do_node_delete(self, args): + """Delete a node.""" + try: + self.node.delete(args.node) + except exc.HTTPNotFound: + raise exc.CommandError('Node not found: %s' % args.node) + + @utils.arg('port', metavar='', help="ID of port") def do_port_show(self, args): """Show a port."""