Added commands to manage deployment sequences

Commands:
  - fuel2 sequence create -r <release> -n <name> -t <graph_type1> [graph_typeN]
  - fuel2 sequence upload -r <release> --file <file_path>
  - fuel2 sequence download <id> [--file <file path>]
  - fuel2 sequence delete <id>
  - fuel2 sequence update <id> [-name <name>] [-t <graph_type1> [graph_typeN]]
  - fuel2 sequence list -r <release_id> | -e <env_id>
  - fuel2 sequence show <id>
  - fuel2 sequence execute <id> -e <env_id> [--force] [--dry-run] [--noop]

DocImpact
Change-Id: I6eb688c5cc91b2b3dbaa2fe5c52a69fe062da664
Partial-Bug: 1620620
This commit is contained in:
Bulat Gaifullin 2016-09-07 14:24:52 +03:00
parent bff20a7123
commit bac19f24b3
12 changed files with 581 additions and 7 deletions

View File

@ -73,6 +73,7 @@ def get_client(resource, version='v1', connection=None):
'plugins': v1.plugins,
'release': v1.release,
'role': v1.role,
'sequence': v1.sequence,
'snapshot': v1.snapshot,
'task': v1.task,
'vip': v1.vip

View File

@ -76,6 +76,8 @@ class BaseCommand(command.Command):
class BaseListCommand(lister.Lister, BaseCommand):
"""Lists all entities showing some information."""
filters = {}
@abc.abstractproperty
def columns(self):
"""Names of columns in the resulting table."""
@ -106,7 +108,13 @@ class BaseListCommand(lister.Lister, BaseCommand):
return parser
def take_action(self, parsed_args):
data = self.client.get_all()
filters = {}
for name, prop in self.filters.items():
value = getattr(parsed_args, prop, None)
if value is not None:
filters[name] = value
data = self.client.get_all(**filters)
data = data_utils.get_display_data_multi(self.columns, data)
scolumn_ids = [self.columns.index(col)

View File

@ -0,0 +1,239 @@
# -*- 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 serializers
from fuelclient.commands import base
from fuelclient.common import data_utils
class SequenceMixIn(object):
entity_name = 'sequence'
class SequenceCreate(SequenceMixIn, base.show.ShowOne, base.BaseCommand):
"""Create a new deployment sequence."""
columns = ("id", "release_id", "name")
def get_parser(self, prog_name):
parser = super(SequenceCreate, self).get_parser(prog_name)
parser.add_argument(
"-r", "--release",
type=int,
required=True,
help="Release object id, sequence will be linked to."
)
parser.add_argument(
'-n', '--name',
required=True,
help='The unique name for sequence'
)
parser.add_argument(
'-t', '--graph-type',
dest='graph_types',
nargs='+',
required=True,
help='Graph types, which will be included to sequence.\n'
'Note: Order is important'
)
return parser
def take_action(self, args):
new_sequence = self.client.create(
args.release, args.name, args.graph_types
)
self.app.stdout.write("Sequence was successfully created:\n")
data = data_utils.get_display_data_single(self.columns, new_sequence)
return self.columns, data
class SequenceUpload(SequenceMixIn, base.show.ShowOne, base.BaseCommand):
"""Upload a new deployment sequence."""
columns = ("id", "release_id", "name")
def get_parser(self, prog_name):
parser = super(SequenceUpload, self).get_parser(prog_name)
parser.add_argument(
"-r", "--release",
type=int,
required=True,
help="Release object id, sequence will be linked to."
)
parser.add_argument(
'--file',
required=True,
help='YAML file which contains deployment sequence properties.'
)
return parser
def take_action(self, args):
serializer = serializers.FileFormatBasedSerializer()
new_sequence = self.client.upload(
args.release, serializer.read_from_file(args.file)
)
self.app.stdout.write("Sequence was successfully created:\n")
data = data_utils.get_display_data_single(self.columns, new_sequence)
return self.columns, data
class SequenceDownload(SequenceMixIn, base.BaseCommand):
"""Download deployment sequence data."""
def get_parser(self, prog_name):
parser = super(SequenceDownload, self).get_parser(prog_name)
parser.add_argument(
"id",
type=int,
help="Sequence ID."
)
parser.add_argument(
'--file',
help='The file path where data will be saved.'
)
return parser
def take_action(self, args):
data = self.client.download(args.id)
if args.file:
serializer = serializers.FileFormatBasedSerializer()
serializer.write_to_file(args.file, data)
else:
serializer = serializers.Serializer("yaml")
serializer.write_to_file(self.app.stdout, data)
class SequenceUpdate(SequenceMixIn, base.BaseShowCommand):
"""Update existing sequence"""
columns = ("id", "name")
def get_parser(self, prog_name):
parser = super(SequenceUpdate, self).get_parser(prog_name)
parser.add_argument(
'-n', '--name',
required=False,
help='The unique name for sequence'
)
parser.add_argument(
'-t', '--graph-type',
dest='graph_types',
nargs='+',
required=False,
help='Graph types, which will be included to sequence.\n'
'Note: Order is important'
)
return parser
def take_action(self, args):
sequence = self.client.update(
args.id, name=args.name, graph_types=args.graph_types
)
if sequence:
self.app.stdout.write("Sequence was successfully updated:\n")
data = data_utils.get_display_data_single(self.columns, sequence)
return self.columns, data
else:
self.app.stdout.write("Nothing to update.\n")
class SequenceDelete(SequenceMixIn, base.BaseDeleteCommand):
"""Delete existing sequence"""
class SequenceShow(SequenceMixIn, base.BaseShowCommand):
"""Display information about sequence"""
columns = ("id", "release_id", "name", "graphs")
class SequenceList(SequenceMixIn, base.BaseListCommand):
"""Delete existing sequence"""
columns = ("id", "release_id", "name")
filters = {'release': 'release', 'cluster': 'env'}
def get_parser(self, prog_name):
parser = super(SequenceList, self).get_parser(prog_name)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
'-r', '--release',
type=int,
help='The Release object ID'
)
group.add_argument(
'-e', '--env',
type=int,
help='The environment object id.\n'
)
return parser
class SequenceExecute(SequenceMixIn, base.BaseCommand):
"""Executes sequence on specified environment."""
def get_parser(self, prog_name):
parser = super(SequenceExecute, self).get_parser(prog_name)
parser.add_argument(
'id',
type=int,
help='Id of the Sequence.'
)
parser.add_argument(
'-e', '--env',
type=int,
required=True,
help='Id of the environment'
)
parser.add_argument(
'--dry-run',
action="store_true",
default=False,
help='Specifies to dry-run a deployment by configuring '
'task executor to dump the deployment graph to a dot file.')
parser.add_argument(
'--force',
action="store_true",
default=False,
help='Force run all deployment tasks '
'without evaluating conditions.'
)
parser.add_argument(
'--noop',
action="store_true",
default=False,
help='Specifies noop-run deployment configuring '
'tasks executor to run puppet and shell tasks in '
'noop mode and skip all other. Stores noop-run '
'result summary in nailgun database.'
)
return parser
def take_action(self, args):
result = self.client.execute(
sequence_id=args.id,
env_id=args.env,
dry_run=args.dry_run,
noop_run=args.noop,
force=args.force
)
msg = 'Deployment task with id {t} for the environment {e} ' \
'has been started.\n'.format(t=result.data['id'],
e=result.data['cluster'])
self.app.stdout.write(msg)

View File

@ -29,3 +29,4 @@ from fuelclient.objects.task import Task
from fuelclient.objects.fuelversion import FuelVersion
from fuelclient.objects.network_group import NetworkGroup
from fuelclient.objects.plugins import Plugins
from fuelclient.objects.sequence import Sequence

View File

@ -60,9 +60,9 @@ class BaseObject(object):
return self._data
@classmethod
def get_all_data(cls):
return cls.connection.get_request(cls.class_api_path)
def get_all_data(cls, **kwargs):
return cls.connection.get_request(cls.class_api_path, params=kwargs)
@classmethod
def get_all(cls):
return map(cls.init_with_data, cls.get_all_data())
def get_all(cls, **kwargs):
return map(cls.init_with_data, cls.get_all_data(**kwargs))

View File

@ -0,0 +1,21 @@
# 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.objects.base import BaseObject
class Sequence(BaseObject):
class_api_path = "sequences/"
instance_api_path = "sequences/{0}/"

View File

@ -0,0 +1,82 @@
# -*- 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
from fuelclient.tests.unit.v2.cli import test_engine
class TestSequenceActions(test_engine.BaseCLITest):
def test_create(self):
self.exec_command(
'sequence create -r 1 -n test -t test_graph'
)
self.m_client.create.assert_called_once_with(
1, 'test', ['test_graph']
)
def test_upload(self):
m_open = mock.mock_open(read_data='name: test\ngraphs: [test]')
module_path = 'fuelclient.cli.serializers.open'
with mock.patch(module_path, m_open, create=True):
self.exec_command(
'sequence upload -r 1 --file sequence.yaml'
)
self.m_client.upload.assert_called_once_with(
1, {'name': 'test', 'graphs': ['test']}
)
def test_download(self):
self.m_client.download.return_value = {"name": "test"}
m_open = mock.mock_open()
module_path = 'fuelclient.cli.serializers.open'
with mock.patch(module_path, m_open, create=True):
self.exec_command(
'sequence download 1 --file sequence.json'
)
self.m_client.download.assert_called_once_with(1)
with mock.patch('sys.stdout') as stdout_mock:
self.exec_command('sequence download 1')
stdout_mock.write.assert_called_with("name: test\n")
def test_update(self):
self.exec_command(
'sequence update 1 -n test -t test_graph'
)
self.m_client.update.assert_called_once_with(
1, name='test', graph_types=['test_graph']
)
def test_show(self):
self.exec_command('sequence show 1')
self.m_client.get_by_id.assert_called_once_with(1)
def test_list(self):
self.exec_command('sequence list -r 1')
self.m_client.get_all.assert_called_once_with(release=1)
def test_delete(self):
self.exec_command('sequence delete 1')
self.m_client.delete_by_id.assert_called_once_with(1)
def test_execute(self):
self.exec_command(
'sequence execute 1 -e 2 --dry-run --force'
)
self.m_client.execute.assert_called_once_with(
sequence_id=1, env_id=2, dry_run=True, noop_run=False, force=True
)

View File

@ -0,0 +1,105 @@
# -*- 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 fuelclient
from fuelclient.tests.unit.v2.lib import test_api
class TestDeploymentSequence(test_api.BaseLibTest):
def setUp(self):
super(TestDeploymentSequence, self).setUp()
self.version = 'v1'
self.client = fuelclient.get_client('sequence', self.version)
self.env_id = 1
self.sequence_body = {
'id': 1, 'release_id': 1, 'name': 'test',
'graphs': [{'type': 'graph1'}]
}
def _check_sequence_object(self, sequence):
self.assertEqual(self.sequence_body, sequence)
def test_create(self):
matcher_post = self.m_request.post(
'/api/v1/sequences/', json=self.sequence_body
)
sequence = self.client.create(1, name='test', graph_types=['graph1'])
self.assertTrue(matcher_post.called)
self._check_sequence_object(sequence)
def test_update(self):
matcher_put = self.m_request.put(
'/api/v1/sequences/1/', json=self.sequence_body
)
sequence = self.client.update(1, name='test')
self.assertTrue(matcher_put.called)
self.assertEqual('{"name": "test"}', matcher_put.last_request.body)
self._check_sequence_object(sequence)
def test_delete(self):
mathcher_delete = self.m_request.delete(
'/api/v1/sequences/1/', status_code=204
)
self.client.delete_by_id(1)
self.assertTrue(mathcher_delete.called)
def test_get_one(self):
matcher_get = self.m_request.get(
'/api/v1/sequences/1/', json=self.sequence_body
)
sequence = self.client.get_by_id(1)
self.assertTrue(matcher_get.called)
self.assertEqual('test', sequence['name'])
self.assertEqual('graph1', sequence['graphs'])
def test_get_all(self):
matcher_get = self.m_request.get(
'/api/v1/sequences/?release=1', json=[self.sequence_body]
)
sequences = self.client.get_all(release=1)
self.assertTrue(matcher_get.called)
self.assertEqual(1, len(sequences))
self._check_sequence_object(sequences[0])
def test_execute(self):
self.m_request.get('/api/v1/nodes/?cluster_id=2', json=[])
self.m_request.get('/api/v1/cluster/2', json={'id': 2})
matcher_post = self.m_request.post(
'/api/v1/sequences/1/execute/', json={'id': 10, 'cluster': 2}
)
self.client.execute(1, 2, dry_run=True, noop_run=False)
self.assertTrue(matcher_post.called)
self.assertIn('"cluster": 2', matcher_post.last_request.body)
self.assertIn('"noop_run": false', matcher_post.last_request.body)
self.assertIn('"dry_run": true', matcher_post.last_request.body)
def test_upload(self):
matcher_post = self.m_request.post(
'/api/v1/sequences/', json=self.sequence_body
)
sequence = self.client.upload(1, self.sequence_body)
self.assertTrue(matcher_post.called)
self._check_sequence_object(sequence)
def test_download(self):
matcher_get = self.m_request.get(
'/api/v1/sequences/1/', json=self.sequence_body
)
sequence = self.client.download(1)
self.assertTrue(matcher_get.called)
self._check_sequence_object(sequence)

View File

@ -25,6 +25,7 @@ from fuelclient.v1 import openstack_config
from fuelclient.v1 import release
from fuelclient.v1 import role
from fuelclient.v1 import plugins
from fuelclient.v1 import sequence
from fuelclient.v1 import snapshot
from fuelclient.v1 import task
from fuelclient.v1 import vip
@ -43,6 +44,7 @@ __all__ = ('cluster_settings',
'plugins',
'release',
'role',
'sequence',
'snapshot',
'task',
'vip')

View File

@ -38,8 +38,8 @@ class BaseV1Client(object):
{'connection': self.connection}
)
def get_all(self):
result = self._entity_wrapper.get_all_data()
def get_all(self, **kwargs):
result = self._entity_wrapper.get_all_data(**kwargs)
return result

107
fuelclient/v1/sequence.py Normal file
View File

@ -0,0 +1,107 @@
# -*- 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 import objects
from fuelclient.v1 import base_v1
class SequenceClient(base_v1.BaseV1Client):
_entity_wrapper = objects.Sequence
executor_path = _entity_wrapper.instance_api_path + 'execute/'
def create(self, release_id, name, graph_types):
"""Creates new sequence object.
:param release_id: the release object id
:param name: the sequence name
:param graph_types: the types of graphs
:returns: created object
"""
data = {'name': name}
graphs = data['graphs'] = []
for graph_type in graph_types:
graphs.append({'type': graph_type})
return self.upload(release_id, data)
def upload(self, release_id, data):
"""Creates new sequence object from data.
:param release_id: release object id
:param data: the sequence properties
:returns: created object
"""
url = self._entity_wrapper.class_api_path
data['release'] = release_id
return self.connection.post_request(url, data)
def download(self, sequence_id):
"""Get raw content of sequence."""
return super(SequenceClient, self).get_by_id(sequence_id)
def update(self, sequence_id, name=None, graph_types=None):
"""Updates existing object.
:param sequence_id: the sequence object id
:param name: new name
:param graph_types: new graph types
:returns: updated object or False if nothing to update
"""
data = {}
if name:
data['name'] = name
if graph_types:
graphs = data['graphs'] = []
for graph_type in graph_types:
graphs.append({'type': graph_type})
if not data:
return False
url = self._entity_wrapper.instance_api_path.format(sequence_id)
return self.connection.put_request(url, data)
def get_by_id(self, sequence_id):
"""Gets formatted sequence data by id."""
data = super(SequenceClient, self).get_by_id(sequence_id)
data['graphs'] = ', '.join(g['type'] for g in data['graphs'])
return data
def delete_by_id(self, sequence_id):
"""Deletes existed object.
:param sequence_id: the sequence object id
"""
url = self._entity_wrapper.instance_api_path.format(sequence_id)
self.connection.delete_request(url)
def execute(self, sequence_id, env_id, **kwargs):
"""Executes sequence on cluster.
:param sequence_id: the sequence object id
:param env_id: the cluster id
:param kwargs: options - force, dry_run and noop.
"""
data = {'cluster': env_id}
data.update(kwargs)
url = self.executor_path.format(sequence_id)
deploy_data = self.connection.post_request(url, data)
return objects.DeployTask.init_with_data(deploy_data)
def get_client(connection):
return SequenceClient(connection)

View File

@ -110,6 +110,14 @@ fuelclient =
task_network-configuration_download=fuelclient.commands.task:TaskNetworkConfigurationDownload
task_settings_download=fuelclient.commands.task:TaskClusterSettingsDownload
task_show=fuelclient.commands.task:TaskShow
sequence_create=fuelclient.commands.sequence:SequenceCreate
sequence_delete=fuelclient.commands.sequence:SequenceDelete
sequence_download=fuelclient.commands.sequence:SequenceDownload
sequence_execute=fuelclient.commands.sequence:SequenceExecute
sequence_list=fuelclient.commands.sequence:SequenceList
sequence_show=fuelclient.commands.sequence:SequenceShow
sequence_update=fuelclient.commands.sequence:SequenceUpdate
sequence_upload=fuelclient.commands.sequence:SequenceUpload
snapshot_create=fuelclient.commands.snapshot:SnapshotGenerate
snapshot_get-default-config=fuelclient.commands.snapshot:SnapshotConfigGetDefault
snapshot_get-link=fuelclient.commands.snapshot:SnapshotGetLink