diff --git a/setup.cfg b/setup.cfg index c905e1503..1d771c34e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -78,6 +78,7 @@ openstack.tripleoclient.v1 = overcloud_plan_list = tripleoclient.v1.overcloud_plan:ListPlans overcloud_profiles_match = tripleoclient.v1.overcloud_profiles:MatchProfiles overcloud_profiles_list = tripleoclient.v1.overcloud_profiles:ListProfiles + overcloud_raid_create = tripleoclient.v1.overcloud_raid:CreateRAID overcloud_update_stack = tripleoclient.v1.overcloud_update:UpdateOvercloud overcloud_execute = tripleoclient.v1.overcloud_execute:RemoteExecute undercloud_install = tripleoclient.v1.undercloud:InstallUndercloud diff --git a/tripleoclient/tests/v1/test_overcloud_raid.py b/tripleoclient/tests/v1/test_overcloud_raid.py new file mode 100644 index 000000000..abbfdcda3 --- /dev/null +++ b/tripleoclient/tests/v1/test_overcloud_raid.py @@ -0,0 +1,135 @@ +# 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 json +import mock +import tempfile + +from osc_lib.tests import utils as test_utils + +from tripleoclient.tests.v1.baremetal import fakes +from tripleoclient.v1 import overcloud_raid + + +class TestCreateRAID(fakes.TestBaremetal): + + def setUp(self): + super(TestCreateRAID, self).setUp() + + self.cmd = overcloud_raid.CreateRAID(self.app, None) + + self.workflow = self.app.client_manager.workflow_engine + self.conf = { + "logical_disks": [ + {"foo": "bar"}, + {"foo2": "bar2"} + ] + } + tripleoclient = self.app.client_manager.tripleoclient + websocket = tripleoclient.messaging_websocket() + websocket.wait_for_message.side_effect = [ + {'status': "SUCCESS"} + ] + self.websocket = websocket + + self.workflow.executions.create.return_value = mock.MagicMock( + output=json.dumps({ + "result": None + }) + ) + + def test_ok(self): + conf = json.dumps(self.conf) + arglist = ['--node', 'uuid1', '--node', 'uuid2', conf] + verifylist = [ + ('node', ['uuid1', 'uuid2']), + ('configuration', conf) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.workflow.executions.create.assert_called_once_with( + 'tripleo.baremetal.v1.create_raid_configuration', + workflow_input={ + 'node_uuids': ['uuid1', 'uuid2'], + 'configuration': self.conf, + 'queue_name': mock.ANY, + } + ) + + def test_from_file(self): + with tempfile.NamedTemporaryFile('w+t') as fp: + json.dump(self.conf, fp) + fp.flush() + arglist = ['--node', 'uuid1', '--node', 'uuid2', fp.name] + verifylist = [ + ('node', ['uuid1', 'uuid2']), + ('configuration', fp.name) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.workflow.executions.create.assert_called_once_with( + 'tripleo.baremetal.v1.create_raid_configuration', + workflow_input={ + 'node_uuids': ['uuid1', 'uuid2'], + 'configuration': self.conf, + 'queue_name': mock.ANY, + } + ) + + def test_no_nodes(self): + arglist = ['{}'] + verifylist = [ + ('configuration', '{}') + ] + self.assertRaises(test_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + self.assertFalse(self.workflow.executions.create.called) + + def test_not_yaml(self): + arglist = ['--node', 'uuid1', '--node', 'uuid2', ':'] + verifylist = [ + ('node', ['uuid1', 'uuid2']), + ('configuration', ':') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaisesRegexp(RuntimeError, 'cannot be parsed as YAML', + self.cmd.take_action, parsed_args) + self.assertFalse(self.workflow.executions.create.called) + + def test_bad_type(self): + for conf in ('[]', '{logical_disks: 42}', '{logical_disks: [42]}'): + arglist = ['--node', 'uuid1', '--node', 'uuid2', conf] + verifylist = [ + ('node', ['uuid1', 'uuid2']), + ('configuration', conf) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(TypeError, self.cmd.take_action, parsed_args) + self.assertFalse(self.workflow.executions.create.called) + + def test_bad_value(self): + conf = '{another_key: [{}]}' + arglist = ['--node', 'uuid1', '--node', 'uuid2', conf] + verifylist = [ + ('node', ['uuid1', 'uuid2']), + ('configuration', conf) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(ValueError, self.cmd.take_action, parsed_args) + self.assertFalse(self.workflow.executions.create.called) diff --git a/tripleoclient/v1/overcloud_raid.py b/tripleoclient/v1/overcloud_raid.py new file mode 100644 index 000000000..d571547d9 --- /dev/null +++ b/tripleoclient/v1/overcloud_raid.py @@ -0,0 +1,79 @@ +# Copyright 2016 Red Hat, 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 logging +import os + +from osc_lib.command import command +from osc_lib.i18n import _ +import uuid +import yaml + +from tripleoclient.workflows import baremetal + + +class CreateRAID(command.Command): + """Create RAID on given nodes""" + + log = logging.getLogger(__name__ + ".CreateRAID") + + def get_parser(self, prog_name): + parser = super(CreateRAID, self).get_parser(prog_name) + parser.add_argument('--node', action='append', required=True, + help=_('Nodes to create RAID on (expected to be ' + 'in manageable state). Can be specified ' + 'multiple times.')) + parser.add_argument('configuration', + help=_('RAID configuration (YAML/JSON string or ' + 'file name).')) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action({args})".format(args=parsed_args)) + + if os.path.exists(parsed_args.configuration): + with open(parsed_args.configuration, 'r') as fp: + configuration = yaml.safe_load(fp.read()) + else: + try: + configuration = yaml.safe_load(parsed_args.configuration) + except yaml.YAMLError as exc: + raise RuntimeError( + _('Configuration is not an existing file and cannot be ' + 'parsed as YAML: %s') % exc) + + # Basic sanity check, we defer the full check to Ironic + try: + disks = configuration['logical_disks'] + except KeyError: + raise ValueError( + _('Configuration must contain key "logical_disks"')) + except TypeError: + raise TypeError( + _('Configuration must be an object, got %r instead') + % configuration) + + if (not isinstance(disks, list) or + not all(isinstance(item, dict) for item in disks)): + raise TypeError( + _('Logical disks list is expected to be a list of objects, ' + 'got %r instead') % disks) + + queue_name = str(uuid.uuid4()) + clients = self.app.client_manager + baremetal.create_raid_configuration(clients, + queue_name=queue_name, + node_uuids=parsed_args.node, + configuration=configuration) diff --git a/tripleoclient/workflows/baremetal.py b/tripleoclient/workflows/baremetal.py index dfe765799..38d0633b5 100644 --- a/tripleoclient/workflows/baremetal.py +++ b/tripleoclient/workflows/baremetal.py @@ -248,3 +248,31 @@ def configure_manageable_nodes(clients, **workflow_input): 'Exception configuring nodes: {}'.format(payload['message'])) print(payload['message']) + + +def create_raid_configuration(clients, **workflow_input): + """Create RAID configuration on nodes. + + Run the tripleo.baremetal.v1.create_raid_configuration Mistral workflow. + """ + + workflow_client = clients.workflow_engine + ooo_client = clients.tripleoclient + queue_name = workflow_input['queue_name'] + + execution = base.start_workflow( + workflow_client, + 'tripleo.baremetal.v1.create_raid_configuration', + workflow_input=workflow_input + ) + + print('Creating RAID configuration for given nodes, this may take time') + + with ooo_client.messaging_websocket(queue_name) as ws: + payload = ws.wait_for_message(execution.id) + + if payload['status'] == 'SUCCESS': + print('Success') + else: + raise RuntimeError( + 'Failed to create RAID: {}'.format(payload['message']))