From c2f8b78686de9dceca8bb9c3f655a7fbbd1375a4 Mon Sep 17 00:00:00 2001 From: Ryan Moe Date: Tue, 21 Jul 2015 17:30:30 -0700 Subject: [PATCH] Add support for network groups Network groups can be created and deleted with a new action named network-group. Only attributes of the network group are managed with these commands (i.e no support for managing IP ranges attached to network groups). Examples: To create a new network group: fuel network-group --create --node-group 1 --name "new network" --release 2 --vlan 100 --cidr 10.0.0.0/24 To delete the specified network groups fuel network-group --delete --network 1 fuel network-group --delete --network 2,3,4 To list all available network groups: fuel network-group list To filter network groups by node group: fuel network-group --node-group 1 Change-Id: I63dce39d0e2308e4df1b07301257ae8c65c94a2b Implements: blueprint templates-for-networking --- fuelclient/cli/actions/__init__.py | 2 + fuelclient/cli/actions/network_group.py | 112 ++++++++++++++++++ fuelclient/cli/arguments.py | 32 +++++ fuelclient/objects/network_group.py | 100 ++++++++++++++++ .../tests/v1/integration/test_client.py | 24 ++++ .../tests/v1/unit/test_network_groups.py | 100 ++++++++++++++++ 6 files changed, 370 insertions(+) create mode 100644 fuelclient/cli/actions/network_group.py create mode 100644 fuelclient/objects/network_group.py create mode 100644 fuelclient/tests/v1/unit/test_network_groups.py diff --git a/fuelclient/cli/actions/__init__.py b/fuelclient/cli/actions/__init__.py index 13a92be..f79b35b 100644 --- a/fuelclient/cli/actions/__init__.py +++ b/fuelclient/cli/actions/__init__.py @@ -27,6 +27,7 @@ from fuelclient.cli.actions.interrupt import ResetAction from fuelclient.cli.actions.interrupt import StopAction from fuelclient.cli.actions.network import NetworkAction from fuelclient.cli.actions.network import NetworkTemplateAction +from fuelclient.cli.actions.network_group import NetworkGroupAction from fuelclient.cli.actions.node import NodeAction from fuelclient.cli.actions.nodegroup import NodeGroupAction from fuelclient.cli.actions.notifications import NotificationsAction @@ -66,6 +67,7 @@ actions_tuple = ( TokenAction, GraphAction, FuelVersionAction, + NetworkGroupAction, ) actions = dict( diff --git a/fuelclient/cli/actions/network_group.py b/fuelclient/cli/actions/network_group.py new file mode 100644 index 0000000..afefb31 --- /dev/null +++ b/fuelclient/cli/actions/network_group.py @@ -0,0 +1,112 @@ +# Copyright 2015 Mirantis, Inc. +# +# 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 fuelclient.cli.actions.base import Action +from fuelclient.cli.actions.base import check_all +import fuelclient.cli.arguments as Args +from fuelclient.cli.arguments import group +from fuelclient.cli.formatting import format_table +from fuelclient.objects.network_group import NetworkGroup +from fuelclient.objects.network_group import NetworkGroupCollection + + +class NetworkGroupAction(Action): + """Show or modify network groups + """ + action_name = "network-group" + acceptable_keys = ("id", "name", "vlan_start", "cidr", + "gateway", "group_id") + + def __init__(self): + super(NetworkGroupAction, self).__init__() + self.args = ( + Args.get_env_arg(), + Args.get_name_arg("Name of new network group."), + Args.get_node_group_arg("ID of node group."), + Args.get_release_arg("Release ID this network group belongs to."), + Args.get_vlan_arg("VLAN of network."), + Args.get_cidr_arg("CIDR of network."), + Args.get_gateway_arg("Gateway of network."), + Args.get_network_group_arg("ID of network group."), + Args.get_meta_arg("Metadata in JSON format to override default " + "network metadata."), + group( + Args.get_create_network_arg( + "Create a new network group for the specified " + " node group." + ), + Args.get_delete_arg("Delete specified network groups."), + Args.get_list_arg("List all network groups.") + ) + ) + self.flag_func_map = ( + ("create", self.create), + ("delete", self.delete), + (None, self.list) + ) + + @check_all('nodegroup', 'name', 'cidr') + def create(self, params): + """Create a new network group + fuel network-group --create --node-group 1 --name "new network" + --release 2 --vlan 100 --cidr 10.0.0.0/24 + """ + NetworkGroup.create( + params.name, + params.release, + params.vlan, + params.cidr, + params.gateway, + int(params.nodegroup.pop()), + params.meta + ) + + self.serializer.print_to_output( + {}, + "Network group {0} has been created".format(params.name) + ) + + @check_all('network') + def delete(self, params): + """Delete the specified network groups + fuel network-group --delete --network 1 + fuel network-group --delete --network 2,3,4 + """ + ngs = NetworkGroup.get_by_ids(params.network) + for n in ngs: + NetworkGroup.delete(n.id) + + self.serializer.print_to_output( + {}, + "Network groups with IDS {0} have been deleted.".format( + ','.join(params.network)) + ) + + def list(self, params): + """To list all available network groups: + fuel network-group list + + To filter them by node group: + fuel network-group --node-group 1 + """ + group_collection = NetworkGroupCollection.get_all() + if params.nodegroup: + group_collection.filter_by_group_id(int(params.nodegroup.pop())) + self.serializer.print_to_output( + group_collection.data, + format_table( + group_collection.data, + acceptable_keys=self.acceptable_keys, + ) + ) diff --git a/fuelclient/cli/arguments.py b/fuelclient/cli/arguments.py index 0911b98..1697a4f 100644 --- a/fuelclient/cli/arguments.py +++ b/fuelclient/cli/arguments.py @@ -439,6 +439,38 @@ def get_group_arg(help_msg): return get_set_type_arg("group", help=help_msg) +def get_node_group_arg(help_msg): + return get_set_type_arg("nodegroup", flags=("--node-group",), + help=help_msg) + + +def get_vlan_arg(help_msg): + return get_int_arg("vlan", help=help_msg) + + +def get_cidr_arg(help_msg): + return get_str_arg("cidr", help=help_msg) + + +def get_gateway_arg(help_msg): + return get_str_arg("gateway", help=help_msg) + + +def get_meta_arg(help_msg): + return get_str_arg("meta", help=help_msg) + + +def get_create_network_arg(help_msg): + return get_boolean_arg( + "create", + flags=("-c", "--create"), + help=help_msg) + + +def get_network_group_arg(help_msg): + return get_set_type_arg("network", help=help_msg) + + def get_release_arg(help_msg, required=False): return get_int_arg( "release", diff --git a/fuelclient/objects/network_group.py b/fuelclient/objects/network_group.py new file mode 100644 index 0000000..01fc198 --- /dev/null +++ b/fuelclient/objects/network_group.py @@ -0,0 +1,100 @@ +# Copyright 2014 Mirantis, Inc. +# +# 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 operator import attrgetter + +from fuelclient.objects.base import BaseObject + + +class NetworkGroup(BaseObject): + + class_api_path = "networks/" + instance_api_path = "networks/{0}/" + + @property + def name(self): + return self.get_fresh_data()["name"] + + @classmethod + def create(cls, name, release, vlan, cidr, gateway, + group_id, meta=None): + + metadata = { + 'notation': 'ip_ranges', + 'render_type': None, + 'map_priority': 2, + 'configurable': True, + 'use_gateway': False, + 'name': name, + 'cidr': cidr, + 'vlan_start': vlan + } + if meta: + metadata.update(meta) + + network_group = { + 'name': name, + 'release': release, + 'vlan_start': vlan, + 'cidr': cidr, + 'gateway': gateway, + 'meta': metadata, + 'group_id': group_id, + } + + return cls.connection.post_request( + cls.class_api_path, + network_group, + ) + + @classmethod + def delete(cls, network_id): + return cls.connection.delete_request( + cls.instance_api_path.format(network_id) + ) + + +class NetworkGroupCollection(object): + + def __init__(self, networks): + self.collection = networks + + @classmethod + def init_with_ids(cls, ids): + return cls(map(NetworkGroup, ids)) + + @classmethod + def init_with_data(cls, data): + return cls(map(NetworkGroup.init_with_data, data)) + + def __str__(self): + return "{0} [{1}]".format( + self.__class__.__name__, + ", ".join(map(lambda n: str(n.id), self.collection)) + ) + + def __iter__(self): + return iter(self.collection) + + @property + def data(self): + return map(attrgetter("data"), self.collection) + + @classmethod + def get_all(cls): + return cls(NetworkGroup.get_all()) + + def filter_by_group_id(self, group_id): + self.collection = filter(lambda net: net.data['group_id'] == group_id, + self.collection) diff --git a/fuelclient/tests/v1/integration/test_client.py b/fuelclient/tests/v1/integration/test_client.py index ff7765f..48ec115 100644 --- a/fuelclient/tests/v1/integration/test_client.py +++ b/fuelclient/tests/v1/integration/test_client.py @@ -239,6 +239,30 @@ class TestHandlers(base.BaseTestCase): msg ) + def test_create_network_group_fails_w_duplicate_name(self): + err_msg = ("(Network with name storage already exists " + "in node group default)\n") + + self.run_cli_commands(( + "env create --name=NewEnv --release=1 --nst=gre", + )) + self.check_for_stderr( + ("network-group --create --name storage --node-group 1 " + "--vlan 10 --cidr 10.0.0.0/24"), + err_msg, + check_errors=False + ) + + def test_create_network_group_fails_w_invalid_group(self): + err_msg = "(Node group with ID 0 does not exist)\n" + + self.check_for_stderr( + ("network-group --create --name test --node-group 0 " + "--vlan 10 --cidr 10.0.0.0/24"), + err_msg, + check_errors=False + ) + class TestUserActions(base.BaseTestCase): diff --git a/fuelclient/tests/v1/unit/test_network_groups.py b/fuelclient/tests/v1/unit/test_network_groups.py new file mode 100644 index 0000000..7f0eef7 --- /dev/null +++ b/fuelclient/tests/v1/unit/test_network_groups.py @@ -0,0 +1,100 @@ +# Copyright 2015 Mirantis, Inc. +# +# 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 mock +import requests_mock + +from fuelclient.tests import base + + +@requests_mock.mock() +class TestNetworkGroupActions(base.UnitTestCase): + + def setUp(self): + super(TestNetworkGroupActions, self).setUp() + + self.env_id = 42 + self.req_base_path = '/api/v1/networks/' + + def test_list_network_groups(self, mreq): + mreq.get(self.req_base_path) + list_commands = [ + ['fuel', 'network-group', '--list'], ['fuel', 'network-group']] + + for cmd in list_commands: + self.execute(cmd) + self.assertEqual(mreq.last_request.method, 'GET') + self.assertEqual(mreq.last_request.path, self.req_base_path) + + def test_list_network_groups_filtering(self, mreq): + mreq.get(self.req_base_path) + + self.execute(['fuel', 'network-group', '--node-group', '1']) + + self.assertEqual(mreq.last_request.method, 'GET') + self.assertEqual(mreq.last_request.path, self.req_base_path) + + def test_create_network_group(self, mreq): + mreq.post(self.req_base_path) + self.execute(['fuel', 'network-group', '--create', '--cidr', + '10.0.0.0/24', '--name', 'test network', + '--node-group', '1']) + + call_data = mreq.last_request.json() + self.assertEqual(1, call_data['group_id']) + self.assertEqual("test network", call_data['name']) + + self.assertEqual(mreq.last_request.method, 'POST') + self.assertEqual(mreq.last_request.path, self.req_base_path) + + def test_create_network_group_required_args(self, mreq): + with mock.patch("sys.stderr") as m_stderr: + with self.assertRaises(SystemExit): + self.execute( + ['fuel', 'network-group', '--create']) + + self.assertIn('--nodegroup", "--name" and "--cidr" required!', + m_stderr.write.call_args[0][0]) + + def test_delete_network_group_required_args(self, mreq): + with mock.patch("sys.stderr") as m_stderr: + with self.assertRaises(SystemExit): + self.execute( + ['fuel', 'network-group', '--delete']) + + self.assertIn('"--network" required!', m_stderr.write.call_args[0][0]) + + def test_delete_network_group(self, mreq): + path = self.req_base_path + str(self.env_id) + '/' + mreq.delete(path) + self.execute( + ['fuel', 'network-group', '--delete', + '--network', str(self.env_id)]) + + self.assertEqual(mreq.last_request.method, 'DELETE') + self.assertEqual(mreq.last_request.path, path) + + def test_network_group_duplicate_name(self, mreq): + mreq.post(self.req_base_path, status_code=409) + + with mock.patch("sys.stderr") as m_stderr: + with self.assertRaises(SystemExit): + self.execute( + ['fuel', 'network-group', '--create', '--cidr', + '10.0.0.0/24', '--name', 'test network', '--node-group', + '1']) + + self.assertIn("409 Client Error", m_stderr.write.call_args[0][0]) + self.assertEqual(mreq.last_request.method, 'POST') + self.assertEqual(mreq.last_request.path, self.req_base_path)