fuel2: add `node ansible-inventory` command

This command makes fuel2 client generate an ansible inventory file
based on the list of cluster nodes. Nodes are grouped by roles, if a
particular node has more than one role, it will appear in the
inventory file several times.

More details on the ansible inventory file format:

   http://docs.ansible.com/ansible/intro_inventory.html

This could be useful when managing Fuel deployed clusters by the
means of ansible.

Change-Id: I2bcb5756ccbeae96f3614e1bda3caf7c8289973f
This commit is contained in:
Roman Podoliaka 2016-03-12 16:18:29 +02:00
parent 4635d4b375
commit de0482ce15
4 changed files with 104 additions and 2 deletions

View File

@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import collections
import operator
import os
import six
@ -322,3 +324,49 @@ class NodeAttributesUpload(NodeMixIn, base.BaseCommand):
self.app.stdout.write(
"Attributes for node {0} were uploaded."
.format(parsed_args.id) + os.linesep)
class NodeAnsibleInventory(NodeMixIn, base.BaseCommand):
"""Generate ansible inventory file based on the nodes list."""
def get_parser(self, prog_name):
parser = super(NodeAnsibleInventory, self).get_parser(prog_name)
# if this is a required argument, we'll avoid ambiguity of having nodes
# of multiple different clusters in the same inventory file
parser.add_argument(
'-e',
'--env',
type=int,
required=True,
help='Use only nodes that are in the specified environment')
parser.add_argument(
'-l',
'--labels',
type=utils.str_to_unicode,
nargs='+',
help='Use only nodes that have specific labels')
return parser
def take_action(self, parsed_args):
data = self.client.get_all(environment_id=parsed_args.env,
labels=parsed_args.labels)
nodes_by_role = collections.defaultdict(list)
for node in data:
for role in node['roles']:
nodes_by_role[role].append(node)
for role, nodes in sorted(nodes_by_role.items()):
self.app.stdout.write(u'[{role}]\n'.format(role=role))
self.app.stdout.write(
u'\n'.join(
u'{name} ansible_host={ip}'.format(name=node['hostname'],
ip=node['ip'])
for node in sorted(nodes_by_role[role],
key=operator.itemgetter('hostname'))
)
)
self.app.stdout.write(u'\n\n')

View File

@ -14,6 +14,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import io
import mock
import six
@ -76,6 +78,56 @@ class TestNodeCommand(test_engine.BaseCLITest):
self.assertIsInstance(
self.m_client.get_all.call_args[1].get('labels')[0], six.text_type)
def test_node_list_ansible_inventory(self):
self.m_client.get_all.return_value = [
fake_node.get_fake_node(hostname='node-1',
ip='10.20.0.2',
roles=['compute']),
fake_node.get_fake_node(hostname='node-2',
ip='10.20.0.3',
roles=['compute', 'ceph-osd']),
fake_node.get_fake_node(hostname='node-3',
ip='10.20.0.4',
roles=['controller']),
fake_node.get_fake_node(hostname='node-4',
ip='10.20.0.5',
roles=['controller', 'mongo']),
fake_node.get_fake_node(hostname='node-5',
ip='10.20.0.6',
roles=['controller', 'ceph-osd']),
]
expected_output = '''\
[ceph-osd]
node-2 ansible_host=10.20.0.3
node-5 ansible_host=10.20.0.6
[compute]
node-1 ansible_host=10.20.0.2
node-2 ansible_host=10.20.0.3
[controller]
node-3 ansible_host=10.20.0.4
node-4 ansible_host=10.20.0.5
node-5 ansible_host=10.20.0.6
[mongo]
node-4 ansible_host=10.20.0.5
'''
args = 'node ansible-inventory --env 1'
with mock.patch('sys.stdout', new=io.StringIO()) as mstdout:
rv = self.exec_command(args)
actual_output = mstdout.getvalue()
self.assertFalse(rv)
self.assertEqual(expected_output, actual_output)
self.m_get_client.assert_called_once_with('node', mock.ANY)
self.m_client.get_all.assert_called_once_with(
environment_id=1, labels=None)
def test_node_show(self):
node_id = 42
args = 'node show {node_id}'.format(node_id=node_id)

View File

@ -17,7 +17,8 @@
def get_fake_node(cluster=None, hostname=None, node_id=None, cpu_model=None,
roles=None, mac=None, memory_b=None, os_platform=None,
status=None, node_name=None, group_id=None, labels=None):
status=None, node_name=None, group_id=None, labels=None,
ip=None):
"""Creates a fake node
Returns the serialized and parametrized representation of a dumped Fuel
@ -35,7 +36,7 @@ def get_fake_node(cluster=None, hostname=None, node_id=None, cpu_model=None,
'error_type': None,
'cluster': cluster or 1,
'id': node_id or 42,
'ip': '10.20.0.4',
'ip': ip or '10.20.0.4',
'kernel_params': None,
'group_id': group_id or 1,
'mac': mac or 'd6:11:3f:b0:f1:43',

View File

@ -56,6 +56,7 @@ fuelclient =
node_list=fuelclient.commands.node:NodeList
node_show=fuelclient.commands.node:NodeShow
node_update=fuelclient.commands.node:NodeUpdate
node_ansible-inventory=fuelclient.commands.node:NodeAnsibleInventory
plugins_sync=fuelclient.commands.plugins:PluginsSync
task_list=fuelclient.commands.task:TaskList
task_show=fuelclient.commands.task:TaskShow