Add custom graph upload and run support to the Fuel V2 CLI
Following commands are added: * fuel2 graph upload --env env_id [--type graph_type] --file tasks.yaml * fuel2 graph upload --release release_id [--type graph_type] --file tasks.yaml * fuel2 graph upload --plugin plugin_id [--type graph_type] --file tasks.yaml --type is optional. ‘default’ graph type with confirmation should be used if no type is defined. * fuel2 graph execute --env env_id [--type graph_type] [--node node_ids] Graph execution available only for the environment. Change-Id: I33b168b7929b10200709efc58fd550f8779b39ae Partial-Bug: #1563851 DocImpact
This commit is contained in:
@@ -51,6 +51,7 @@ def get_client(resource, version='v1'):
|
||||
'deployment_history': v1.deployment_history,
|
||||
'environment': v1.environment,
|
||||
'fuel-version': v1.fuelversion,
|
||||
'graph': v1.graph,
|
||||
'network-group': v1.network_group,
|
||||
'node': v1.node,
|
||||
'openstack-config': v1.openstack_config,
|
||||
|
||||
150
fuelclient/commands/graph.py
Normal file
150
fuelclient/commands/graph.py
Normal file
@@ -0,0 +1,150 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 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 os
|
||||
|
||||
from fuelclient.cli import error
|
||||
from fuelclient.cli.serializers import Serializer
|
||||
from fuelclient.commands import base
|
||||
|
||||
|
||||
class FileMethodsMixin(object):
|
||||
@classmethod
|
||||
def check_file_path(cls, file_path):
|
||||
if not os.path.exists(file_path):
|
||||
raise error.InvalidFileException(
|
||||
"File '{0}' doesn't exist.".format(file_path))
|
||||
|
||||
@classmethod
|
||||
def check_dir(cls, directory):
|
||||
if not os.path.exists(directory):
|
||||
raise error.InvalidDirectoryException(
|
||||
"Directory '{0}' doesn't exist.".format(directory))
|
||||
if not os.path.isdir(directory):
|
||||
raise error.InvalidDirectoryException(
|
||||
"Error: '{0}' is not a directory.".format(directory))
|
||||
|
||||
|
||||
class GraphUpload(base.BaseCommand, FileMethodsMixin):
|
||||
"""Upload deployment graph configuration."""
|
||||
entity_name = 'graph'
|
||||
|
||||
@classmethod
|
||||
def read_tasks_data_from_file(cls, file_path=None, serializer=None):
|
||||
"""Read Tasks data from given path.
|
||||
|
||||
:param file_path: path
|
||||
:type file_path: str
|
||||
:param serializer: serializer object
|
||||
:type serializer: object
|
||||
:return: data
|
||||
:rtype: list|object
|
||||
"""
|
||||
cls.check_file_path(file_path)
|
||||
return (serializer or Serializer()).read_from_full_path(file_path)
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(GraphUpload, self).get_parser(prog_name)
|
||||
graph_class = parser.add_mutually_exclusive_group()
|
||||
|
||||
graph_class.add_argument('-e',
|
||||
'--env',
|
||||
type=int,
|
||||
required=False,
|
||||
help='Id of the environment')
|
||||
graph_class.add_argument('-r',
|
||||
'--release',
|
||||
type=int,
|
||||
required=False,
|
||||
help='Id of the release')
|
||||
graph_class.add_argument('-p',
|
||||
'--plugin',
|
||||
type=int,
|
||||
required=False,
|
||||
help='Id of the plugin')
|
||||
|
||||
parser.add_argument('-t',
|
||||
'--type',
|
||||
type=str,
|
||||
default=None,
|
||||
required=False,
|
||||
help='Type of the deployment graph')
|
||||
parser.add_argument('-f',
|
||||
'--file',
|
||||
type=str,
|
||||
required=True,
|
||||
default=None,
|
||||
help='YAML file that contains '
|
||||
'deployment graph data.')
|
||||
return parser
|
||||
|
||||
def take_action(self, args):
|
||||
parameters_to_graph_class = (
|
||||
('env', 'clusters'),
|
||||
('release', 'releases'),
|
||||
('plugin', 'plugins'),
|
||||
)
|
||||
|
||||
for parameter, graph_class in parameters_to_graph_class:
|
||||
model_id = getattr(args, parameter)
|
||||
if model_id:
|
||||
self.client.upload(
|
||||
data=self.read_tasks_data_from_file(args.file),
|
||||
related_model=graph_class,
|
||||
related_id=model_id,
|
||||
graph_type=args.type
|
||||
)
|
||||
break
|
||||
|
||||
self.app.stdout.write(
|
||||
"Deployment graph was uploaded from {0}\n".format(args.file)
|
||||
)
|
||||
|
||||
|
||||
class GraphExecute(base.BaseCommand):
|
||||
"""Start deployment with given graph type."""
|
||||
entity_name = 'graph'
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(GraphExecute, self).get_parser(prog_name)
|
||||
parser.add_argument('-e',
|
||||
'--env',
|
||||
type=int,
|
||||
required=True,
|
||||
help='Id of the environment')
|
||||
parser.add_argument('-t',
|
||||
'--type',
|
||||
type=str,
|
||||
default=None,
|
||||
required=False,
|
||||
help='Type of the deployment graph')
|
||||
parser.add_argument('-n',
|
||||
'--nodes',
|
||||
type=int,
|
||||
nargs='+',
|
||||
required=False,
|
||||
help='Ids of the nodes to use for deployment.')
|
||||
return parser
|
||||
|
||||
def take_action(self, args):
|
||||
self.client.execute(
|
||||
env_id=args.env,
|
||||
graph_type=args.type,
|
||||
nodes=args.nodes
|
||||
)
|
||||
self.app.stdout.write(
|
||||
"Deployment was executed\n"
|
||||
)
|
||||
88
fuelclient/tests/unit/v2/cli/test_deployment_graph.py
Normal file
88
fuelclient/tests/unit/v2/cli/test_deployment_graph.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 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 yaml
|
||||
|
||||
from fuelclient.tests.unit.v2.cli import test_engine
|
||||
|
||||
|
||||
TASKS_YAML = '''- id: custom-task-1
|
||||
type: puppet
|
||||
parameters:
|
||||
param: value
|
||||
- id: custom-task-2
|
||||
type: puppet
|
||||
parameters:
|
||||
param: value
|
||||
'''
|
||||
|
||||
|
||||
class TestGraphActions(test_engine.BaseCLITest):
|
||||
|
||||
@mock.patch('fuelclient.commands.graph.os')
|
||||
def _test_cmd(self, method, cmd_line, expected_kwargs, os_m):
|
||||
os_m.exists.return_value = True
|
||||
self.m_get_client.reset_mock()
|
||||
self.m_client.get_filtered.reset_mock()
|
||||
m_open = mock.mock_open(read_data=TASKS_YAML)
|
||||
with mock.patch(
|
||||
'fuelclient.cli.serializers.open', m_open, create=True):
|
||||
self.exec_command('graph {0} {1}'.format(method, cmd_line))
|
||||
self.m_get_client.assert_called_once_with('graph', mock.ANY)
|
||||
self.m_client.__getattr__(method).assert_called_once_with(
|
||||
**expected_kwargs)
|
||||
|
||||
def test_upload(self):
|
||||
self._test_cmd('upload', '--env 1 --file new_graph.yaml', dict(
|
||||
data=yaml.load(TASKS_YAML),
|
||||
related_model='clusters',
|
||||
related_id=1,
|
||||
graph_type=None
|
||||
))
|
||||
self._test_cmd('upload', '--release 1 --file new_graph.yaml', dict(
|
||||
data=yaml.load(TASKS_YAML),
|
||||
related_model='releases',
|
||||
related_id=1,
|
||||
graph_type=None
|
||||
))
|
||||
self._test_cmd('upload', '--plugin 1 --file new_graph.yaml', dict(
|
||||
data=yaml.load(TASKS_YAML),
|
||||
related_model='plugins',
|
||||
related_id=1,
|
||||
graph_type=None
|
||||
))
|
||||
self._test_cmd(
|
||||
'upload',
|
||||
'--plugin 1 --file new_graph.yaml --type custom_type',
|
||||
dict(
|
||||
data=yaml.load(TASKS_YAML),
|
||||
related_model='plugins',
|
||||
related_id=1,
|
||||
graph_type='custom_type'
|
||||
)
|
||||
)
|
||||
|
||||
def test_execute(self):
|
||||
self._test_cmd(
|
||||
'execute',
|
||||
'--env 1 --type custom_graph --nodes 1 2 3',
|
||||
dict(
|
||||
env_id=1,
|
||||
graph_type='custom_graph',
|
||||
nodes=[1, 2, 3]
|
||||
)
|
||||
)
|
||||
117
fuelclient/tests/unit/v2/lib/test_deployment_graph.py
Normal file
117
fuelclient/tests/unit/v2/lib/test_deployment_graph.py
Normal file
@@ -0,0 +1,117 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 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 yaml
|
||||
|
||||
import fuelclient
|
||||
from fuelclient.tests.unit.v2.lib import test_api
|
||||
from fuelclient.tests.utils import fake_task
|
||||
|
||||
TASKS_YAML = '''- id: custom-task-1
|
||||
type: puppet
|
||||
parameters:
|
||||
param: value
|
||||
- id: custom-task-2
|
||||
type: puppet
|
||||
parameters:
|
||||
param: value
|
||||
'''
|
||||
|
||||
|
||||
class TestDeploymentGraphFacade(test_api.BaseLibTest):
|
||||
|
||||
def setUp(self):
|
||||
super(TestDeploymentGraphFacade, self).setUp()
|
||||
self.version = 'v1'
|
||||
self.client = fuelclient.get_client('graph', self.version)
|
||||
self.env_id = 1
|
||||
|
||||
def test_existing_graph_upload(self):
|
||||
expected_body = {
|
||||
'tasks': yaml.load(TASKS_YAML)}
|
||||
|
||||
matcher_post = self.m_request.post(
|
||||
'/api/v1/clusters/1/deployment_graphs/custom_graph',
|
||||
json=expected_body)
|
||||
|
||||
matcher_get = self.m_request.get(
|
||||
'/api/v1/clusters/1/deployment_graphs/custom_graph',
|
||||
status_code=404,
|
||||
json={'status': 'error', 'message': 'Does not exist'})
|
||||
|
||||
m_open = mock.mock_open(read_data=TASKS_YAML)
|
||||
with mock.patch(
|
||||
'fuelclient.cli.serializers.open', m_open, create=True):
|
||||
self.client.upload(
|
||||
data=expected_body,
|
||||
related_model='clusters',
|
||||
related_id=1,
|
||||
graph_type='custom_graph'
|
||||
)
|
||||
|
||||
self.assertTrue(matcher_get.called)
|
||||
self.assertTrue(matcher_post.called)
|
||||
self.assertItemsEqual(
|
||||
expected_body,
|
||||
matcher_post.last_request.json()
|
||||
)
|
||||
|
||||
def test_new_graph_upload(self):
|
||||
expected_body = {
|
||||
'tasks': yaml.load(TASKS_YAML)}
|
||||
|
||||
matcher_put = self.m_request.put(
|
||||
'/api/v1/clusters/1/deployment_graphs/custom_graph',
|
||||
json=expected_body)
|
||||
|
||||
matcher_get = self.m_request.get(
|
||||
'/api/v1/clusters/1/deployment_graphs/custom_graph',
|
||||
status_code=200,
|
||||
json={
|
||||
'tasks': [{'id': 'imatask', 'type': 'puppet'}]
|
||||
})
|
||||
|
||||
m_open = mock.mock_open(read_data=TASKS_YAML)
|
||||
with mock.patch(
|
||||
'fuelclient.cli.serializers.open', m_open, create=True):
|
||||
self.client.upload(
|
||||
data=expected_body,
|
||||
related_model='clusters',
|
||||
related_id=1,
|
||||
graph_type='custom_graph')
|
||||
|
||||
self.assertTrue(matcher_get.called)
|
||||
self.assertTrue(matcher_put.called)
|
||||
self.assertItemsEqual(
|
||||
expected_body,
|
||||
matcher_put.last_request.json()
|
||||
)
|
||||
|
||||
def test_new_graph_run(self):
|
||||
matcher_put = self.m_request.put(
|
||||
'/api/v1/clusters/1/deploy/?nodes=1,2,3&graph_type=custom_graph',
|
||||
json=fake_task.get_fake_task(cluster=370))
|
||||
# this is required to form running task info
|
||||
self.m_request.get(
|
||||
'/api/v1/nodes/?cluster_id=370',
|
||||
json={}
|
||||
)
|
||||
self.client.execute(
|
||||
env_id=1,
|
||||
nodes=[1, 2, 3],
|
||||
graph_type="custom_graph")
|
||||
self.assertTrue(matcher_put.called)
|
||||
@@ -25,6 +25,7 @@ def get_fake_task(task_id=None, status=None, name=None,
|
||||
"""
|
||||
return {'status': status or 'running',
|
||||
'name': name or 'deploy',
|
||||
'id': task_id or 42,
|
||||
'task_id': task_id or 42,
|
||||
'cluster': cluster or 34,
|
||||
'result': result or '',
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
from fuelclient.v1 import deployment_history
|
||||
from fuelclient.v1 import environment
|
||||
from fuelclient.v1 import fuelversion
|
||||
from fuelclient.v1 import graph
|
||||
from fuelclient.v1 import network_group
|
||||
from fuelclient.v1 import node
|
||||
from fuelclient.v1 import openstack_config
|
||||
@@ -26,6 +27,7 @@ from fuelclient.v1 import vip
|
||||
__all__ = ('deployment_history',
|
||||
'environment',
|
||||
'fuelversion',
|
||||
'graph',
|
||||
'network_group',
|
||||
'node',
|
||||
'openstack_config',
|
||||
|
||||
97
fuelclient/v1/graph.py
Normal file
97
fuelclient/v1/graph.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 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 import error
|
||||
from fuelclient.client import APIClient
|
||||
from fuelclient import objects
|
||||
from fuelclient.v1 import base_v1
|
||||
|
||||
|
||||
class GraphClient(base_v1.BaseV1Client):
|
||||
_entity_wrapper = objects.Environment
|
||||
|
||||
related_graphs_list_api_path = "{related_model}/{related_model_id}" \
|
||||
"/deployment_graphs/"
|
||||
|
||||
related_graph_api_path = "{related_model}/{related_model_id}" \
|
||||
"/deployment_graphs/{graph_type}"
|
||||
|
||||
cluster_deploy_api_path = "clusters/{env_id}/deploy/"
|
||||
|
||||
@classmethod
|
||||
def update_graph_for_model(
|
||||
cls, data, related_model, related_model_id, graph_type=None):
|
||||
return APIClient.put_request(
|
||||
cls.related_graph_api_path.format(
|
||||
related_model=related_model,
|
||||
related_model_id=related_model_id,
|
||||
graph_type=graph_type or ""),
|
||||
data
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_graph_for_model(
|
||||
cls, data, related_model, related_model_id, graph_type=None):
|
||||
return APIClient.post_request(
|
||||
cls.related_graph_api_path.format(
|
||||
related_model=related_model,
|
||||
related_model_id=related_model_id,
|
||||
graph_type=graph_type or ""),
|
||||
data
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_graph_for_model(
|
||||
cls, related_model, related_model_id, graph_type=None):
|
||||
return APIClient.get_request(
|
||||
cls.related_graph_api_path.format(
|
||||
related_model=related_model,
|
||||
related_model_id=related_model_id,
|
||||
graph_type=graph_type or ""))
|
||||
|
||||
def upload(self, data, related_model, related_id, graph_type):
|
||||
# create or update
|
||||
try:
|
||||
self.get_graph_for_model(
|
||||
related_model, related_id, graph_type)
|
||||
self.update_graph_for_model(
|
||||
{'tasks': data}, related_model, related_id, graph_type)
|
||||
except error.HTTPError as exc:
|
||||
if '404' in exc.message:
|
||||
self.create_graph_for_model(
|
||||
{'tasks': data}, related_model, related_id, graph_type)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, env_id, nodes, graph_type=None):
|
||||
put_args = []
|
||||
|
||||
if nodes:
|
||||
put_args.append("nodes={0}".format(",".join(map(str, nodes))))
|
||||
|
||||
if graph_type:
|
||||
put_args.append(("graph_type=" + graph_type))
|
||||
|
||||
url = "".join([
|
||||
cls.cluster_deploy_api_path.format(env_id=env_id),
|
||||
'?',
|
||||
'&'.join(put_args)])
|
||||
|
||||
deploy_data = APIClient.put_request(url, {})
|
||||
return objects.DeployTask.init_with_data(deploy_data)
|
||||
|
||||
|
||||
def get_client():
|
||||
return GraphClient()
|
||||
@@ -39,6 +39,8 @@ fuelclient =
|
||||
env_spawn-vms=fuelclient.commands.environment:EnvSpawnVms
|
||||
env_update=fuelclient.commands.environment:EnvUpdate
|
||||
fuel-version=fuelclient.commands.fuelversion:FuelVersion
|
||||
graph_execute=fuelclient.commands.graph:GraphExecute
|
||||
graph_upload=fuelclient.commands.graph:GraphUpload
|
||||
network-group_create=fuelclient.commands.network_group:NetworkGroupCreate
|
||||
network-group_delete=fuelclient.commands.network_group:NetworkGroupDelete
|
||||
network-group_list=fuelclient.commands.network_group:NetworkGroupList
|
||||
|
||||
Reference in New Issue
Block a user