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, 'plugins': v1.plugins,
'release': v1.release, 'release': v1.release,
'role': v1.role, 'role': v1.role,
'sequence': v1.sequence,
'snapshot': v1.snapshot, 'snapshot': v1.snapshot,
'task': v1.task, 'task': v1.task,
'vip': v1.vip 'vip': v1.vip

View File

@ -76,6 +76,8 @@ class BaseCommand(command.Command):
class BaseListCommand(lister.Lister, BaseCommand): class BaseListCommand(lister.Lister, BaseCommand):
"""Lists all entities showing some information.""" """Lists all entities showing some information."""
filters = {}
@abc.abstractproperty @abc.abstractproperty
def columns(self): def columns(self):
"""Names of columns in the resulting table.""" """Names of columns in the resulting table."""
@ -106,7 +108,13 @@ class BaseListCommand(lister.Lister, BaseCommand):
return parser return parser
def take_action(self, parsed_args): 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) data = data_utils.get_display_data_multi(self.columns, data)
scolumn_ids = [self.columns.index(col) 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.fuelversion import FuelVersion
from fuelclient.objects.network_group import NetworkGroup from fuelclient.objects.network_group import NetworkGroup
from fuelclient.objects.plugins import Plugins from fuelclient.objects.plugins import Plugins
from fuelclient.objects.sequence import Sequence

View File

@ -60,9 +60,9 @@ class BaseObject(object):
return self._data return self._data
@classmethod @classmethod
def get_all_data(cls): def get_all_data(cls, **kwargs):
return cls.connection.get_request(cls.class_api_path) return cls.connection.get_request(cls.class_api_path, params=kwargs)
@classmethod @classmethod
def get_all(cls): def get_all(cls, **kwargs):
return map(cls.init_with_data, cls.get_all_data()) 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 release
from fuelclient.v1 import role from fuelclient.v1 import role
from fuelclient.v1 import plugins from fuelclient.v1 import plugins
from fuelclient.v1 import sequence
from fuelclient.v1 import snapshot from fuelclient.v1 import snapshot
from fuelclient.v1 import task from fuelclient.v1 import task
from fuelclient.v1 import vip from fuelclient.v1 import vip
@ -43,6 +44,7 @@ __all__ = ('cluster_settings',
'plugins', 'plugins',
'release', 'release',
'role', 'role',
'sequence',
'snapshot', 'snapshot',
'task', 'task',
'vip') 'vip')

View File

@ -38,8 +38,8 @@ class BaseV1Client(object):
{'connection': self.connection} {'connection': self.connection}
) )
def get_all(self): def get_all(self, **kwargs):
result = self._entity_wrapper.get_all_data() result = self._entity_wrapper.get_all_data(**kwargs)
return result 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_network-configuration_download=fuelclient.commands.task:TaskNetworkConfigurationDownload
task_settings_download=fuelclient.commands.task:TaskClusterSettingsDownload task_settings_download=fuelclient.commands.task:TaskClusterSettingsDownload
task_show=fuelclient.commands.task:TaskShow 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_create=fuelclient.commands.snapshot:SnapshotGenerate
snapshot_get-default-config=fuelclient.commands.snapshot:SnapshotConfigGetDefault snapshot_get-default-config=fuelclient.commands.snapshot:SnapshotConfigGetDefault
snapshot_get-link=fuelclient.commands.snapshot:SnapshotGetLink snapshot_get-link=fuelclient.commands.snapshot:SnapshotGetLink