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:
Ilya Kutukov
2016-03-30 14:18:18 +03:00
parent 40fb8f84e9
commit 7a9cd8abec
8 changed files with 458 additions and 0 deletions

View File

@@ -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,

View 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"
)

View 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]
)
)

View 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)

View File

@@ -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 '',

View File

@@ -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
View 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()

View File

@@ -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