diff --git a/fuelclient/cli/actions/__init__.py b/fuelclient/cli/actions/__init__.py index d8136e4..ea12ac6 100644 --- a/fuelclient/cli/actions/__init__.py +++ b/fuelclient/cli/actions/__init__.py @@ -25,6 +25,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.node import NodeAction +from fuelclient.cli.actions.nodegroup import NodeGroupAction from fuelclient.cli.actions.release import ReleaseAction from fuelclient.cli.actions.role import RoleAction from fuelclient.cli.actions.settings import SettingsAction @@ -49,7 +50,8 @@ actions_tuple = ( SnapshotAction, HealthCheckAction, UserAction, - PluginAction + PluginAction, + NodeGroupAction ) actions = dict( diff --git a/fuelclient/cli/actions/node.py b/fuelclient/cli/actions/node.py index 53cad77..84579e9 100644 --- a/fuelclient/cli/actions/node.py +++ b/fuelclient/cli/actions/node.py @@ -33,7 +33,7 @@ class NodeAction(Action): """ action_name = "node" acceptable_keys = ("id", "status", "name", "cluster", "ip", - "mac", "roles", "pending_roles", "online") + "mac", "roles", "pending_roles", "online", "group_id") def __init__(self): super(NodeAction, self).__init__() @@ -49,7 +49,7 @@ class NodeAction(Action): Args.get_delete_from_db_arg( "Delete specific nodes only from fuel db.\n" "User should still delete node from cobbler"), - Args.get_provision_arg("Provision specific nodes.") + Args.get_provision_arg("Provision specific nodes."), ), group( Args.get_default_arg( diff --git a/fuelclient/cli/actions/nodegroup.py b/fuelclient/cli/actions/nodegroup.py new file mode 100644 index 0000000..2354618 --- /dev/null +++ b/fuelclient/cli/actions/nodegroup.py @@ -0,0 +1,106 @@ +# 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 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.error import ActionException +from fuelclient.cli.formatting import format_table +from fuelclient.objects.node import Node +from fuelclient.objects.nodegroup import NodeGroup +from fuelclient.objects.nodegroup import NodeGroupCollection + + +class NodeGroupAction(Action): + """Show or modify node groups + """ + action_name = "nodegroup" + acceptable_keys = ("id", "cluster", "name") + + def __init__(self): + super(NodeGroupAction, self).__init__() + self.args = ( + Args.get_env_arg(), + Args.get_list_arg("List all node groups."), + Args.get_name_arg("Name of new node group."), + Args.get_group_arg("ID of node group."), + Args.get_node_arg("List of nodes to assign specified group to."), + group( + Args.get_create_arg( + "Create a new node group in the specified environment." + ), + Args.get_assign_arg( + "Download current network configuration."), + Args.get_delete_arg( + "Verify current network configuration."), + ) + ) + self.flag_func_map = ( + ("create", self.create), + ("delete", self.delete), + ("assign", self.assign), + (None, self.list) + ) + + def create(self, params): + """Create a new node group + fuel --env 1 nodegroup --create --name "group 1" + """ + NodeGroup.create(params.name, int(params.env)) + + def delete(self, params): + """Delete the specified node groups + fuel --env 1 nodegroup --delete --group 1 + fuel --env 1 nodegroup --delete --group 2,3,4 + """ + ngs = NodeGroup.get_by_ids(params.group) + for n in ngs: + if n.name == "default": + raise ActionException( + "Default node groups cannot be deleted." + ) + NodeGroup.delete(n.id) + + @check_all("env") + def assign(self, params): + """Assign nodes to specified node group: + fuel --env 1 nodegroup --assign --node 1 --group 1 + fuel --env 1 nodegroup --assign --node 2,3,4 --group 1 + """ + nodes = [n.id for n in map(Node, params.node)] + ngs = map(NodeGroup, params.group) + if len(ngs) > 1: + raise ActionException( + "Nodes can only be assigned to one node group." + ) + NodeGroup.assign(ngs[0].id, nodes) + + def list(self, params): + """To list all available node groups: + fuel nodegroup + + To filter them by environment: + fuel --env-id 1 nodegroup + """ + group_collection = NodeGroupCollection.get_all() + if params.env: + group_collection.filter_by_env_id(int(params.env)) + 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 ce8cac6..d3536be 100644 --- a/fuelclient/cli/arguments.py +++ b/fuelclient/cli/arguments.py @@ -337,6 +337,14 @@ def get_delete_arg(help_msg): return get_boolean_arg("delete", help=help_msg) +def get_assign_arg(help_msg): + return get_boolean_arg("assign", help=help_msg) + + +def get_group_arg(help_msg): + return get_set_type_arg("group", help=help_msg) + + def get_release_arg(help_msg, required=False): return get_int_arg( "release", diff --git a/fuelclient/objects/nodegroup.py b/fuelclient/objects/nodegroup.py new file mode 100644 index 0000000..bd99365 --- /dev/null +++ b/fuelclient/objects/nodegroup.py @@ -0,0 +1,87 @@ +# 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 NodeGroup(BaseObject): + + class_api_path = "nodegroups/" + instance_api_path = "nodegroups/{0}/" + + @property + def env_id(self): + return self.get_fresh_data()["cluster"] + + @property + def name(self): + return self.get_fresh_data()["name"] + + @classmethod + def create(cls, name, cluster_id): + return cls.connection.post_request( + cls.class_api_path, + {'cluster_id': cluster_id, 'name': name}, + ) + + @classmethod + def delete(cls, group_id): + return cls.connection.delete_request( + cls.instance_api_path.format(group_id) + ) + + @classmethod + def assign(cls, group_id, nodes): + return cls.connection.post_request( + cls.instance_api_path.format(group_id), + nodes + ) + + +class NodeGroupCollection(object): + + def __init__(self, groups): + self.collection = groups + + @classmethod + def init_with_ids(cls, ids): + return cls(map(NodeGroup, ids)) + + @classmethod + def init_with_data(cls, data): + return cls(map(NodeGroup.init_with_data, data)) + + def __str__(self): + return "node groups [{0}]".format( + ", ".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(NodeGroup.get_all()) + + def filter_by_env_id(self, env_id): + self.collection = filter( + lambda group: group.env_id == env_id, + self.collection + )