OpenStackClient plugin for stack show
This change was derived from Id188cdc0e97b480875a7626443f68e69863f0647 which added stack support to python-openstackclient, but adapting it to be an openstackclient plugin which lives in the python-heatclient tree. This change only implements the "openstack stack show". Subsequent commands will remain in WIP changes at the end of this series until they are ready to merge. The stack show formatting has the following behaviour: - specifying key order for important values, but adding all other stack keys in default order (future proofing for new values) - exclude template_description, its a dupe of 'description' - complex values like parameters, outputs and links get a special formatter depending on the output format ('table' formats them as yaml, 'shell', 'value', 'html' formats them as json) Co-Authored-By: Ryan S. Brown <rybrown@redhat.com> Co-Authored-By: Rico Lin <rico.l@inwinstack.com> Change-Id: I3096b94146a94d184c29b8c7c9f6c032eed5281dchanges/67/195867/11
parent
dc6ec87f59
commit
87b559d940
@ -0,0 +1,59 @@
|
||||
# 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
|
||||
|
||||
from openstackclient.common import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_ORCHESTRATION_API_VERSION = '1'
|
||||
API_VERSION_OPTION = 'os_orchestration_api_version'
|
||||
API_NAME = 'orchestration'
|
||||
API_VERSIONS = {
|
||||
'1': 'heatclient.v1.client.Client',
|
||||
}
|
||||
|
||||
|
||||
def make_client(instance):
|
||||
"""Returns an orchestration service client"""
|
||||
heat_client = utils.get_client_class(
|
||||
API_NAME,
|
||||
instance._api_version[API_NAME],
|
||||
API_VERSIONS)
|
||||
LOG.debug('Instantiating orchestration client: %s', heat_client)
|
||||
|
||||
client = heat_client(
|
||||
endpoint=instance.get_endpoint_for_service_type('orchestration'),
|
||||
session=instance.session,
|
||||
auth_url=instance._auth_url,
|
||||
username=instance._username,
|
||||
password=instance._password,
|
||||
region_name=instance._region_name,
|
||||
)
|
||||
|
||||
return client
|
||||
|
||||
|
||||
def build_option_parser(parser):
|
||||
"""Hook to add global options"""
|
||||
parser.add_argument(
|
||||
'--os-orchestration-api-version',
|
||||
metavar='<orchestration-api-version>',
|
||||
default=utils.env(
|
||||
'OS_ORCHESTRATION_API_VERSION',
|
||||
default=DEFAULT_ORCHESTRATION_API_VERSION),
|
||||
help='Orchestration API version, default=' +
|
||||
DEFAULT_ORCHESTRATION_API_VERSION +
|
||||
' (Env: OS_ORCHESTRATION_API_VERSION)')
|
||||
return parser
|
@ -0,0 +1,84 @@
|
||||
# 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.
|
||||
#
|
||||
|
||||
"""Orchestration v1 Stack action implementations"""
|
||||
|
||||
import logging
|
||||
|
||||
from cliff import show
|
||||
from openstackclient.common import exceptions as exc
|
||||
from openstackclient.common import utils
|
||||
|
||||
from heatclient.common import utils as heat_utils
|
||||
from heatclient import exc as heat_exc
|
||||
|
||||
|
||||
class ShowStack(show.ShowOne):
|
||||
"""Show stack details"""
|
||||
|
||||
log = logging.getLogger(__name__ + ".ShowStack")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ShowStack, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'stack',
|
||||
metavar='<stack>',
|
||||
help='Stack to display (name or ID)',
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)", parsed_args)
|
||||
|
||||
heat_client = self.app.client_manager.orchestration
|
||||
return _show_stack(heat_client, stack_id=parsed_args.stack,
|
||||
format=parsed_args.formatter)
|
||||
|
||||
|
||||
def _show_stack(heat_client, stack_id, format):
|
||||
try:
|
||||
data = heat_client.stacks.get(stack_id=stack_id)
|
||||
except heat_exc.HTTPNotFound:
|
||||
raise exc.CommandError('Stack not found: %s' % stack_id)
|
||||
else:
|
||||
|
||||
columns = [
|
||||
'id',
|
||||
'stack_name',
|
||||
'description',
|
||||
'creation_time',
|
||||
'updated_time',
|
||||
'stack_status',
|
||||
'stack_status_reason',
|
||||
'parameters',
|
||||
'outputs',
|
||||
'links',
|
||||
]
|
||||
exclude_columns = ('template_description',)
|
||||
for key in data.to_dict():
|
||||
# add remaining columns without an explicit order
|
||||
if key not in columns and key not in exclude_columns:
|
||||
columns.append(key)
|
||||
formatters = {}
|
||||
complex_formatter = None
|
||||
if format in 'table':
|
||||
complex_formatter = heat_utils.yaml_formatter
|
||||
elif format in ('shell', 'value', 'html'):
|
||||
complex_formatter = heat_utils.json_formatter
|
||||
if complex_formatter:
|
||||
formatters['parameters'] = complex_formatter
|
||||
formatters['outputs'] = complex_formatter
|
||||
formatters['links'] = complex_formatter
|
||||
|
||||
return columns, utils.get_item_properties(data, columns,
|
||||
formatters=formatters)
|
@ -0,0 +1,237 @@
|
||||
# Copyright 2013 Nebula 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 json
|
||||
import sys
|
||||
|
||||
import requests
|
||||
import six
|
||||
|
||||
|
||||
AUTH_TOKEN = "foobar"
|
||||
AUTH_URL = "http://0.0.0.0"
|
||||
USERNAME = "itchy"
|
||||
PASSWORD = "scratchy"
|
||||
TEST_RESPONSE_DICT = {
|
||||
"access": {
|
||||
"metadata": {
|
||||
"is_admin": 0,
|
||||
"roles": [
|
||||
"1234",
|
||||
]
|
||||
},
|
||||
"serviceCatalog": [
|
||||
{
|
||||
"endpoints": [
|
||||
{
|
||||
"adminURL": AUTH_URL + "/v2.0",
|
||||
"id": "1234",
|
||||
"internalURL": AUTH_URL + "/v2.0",
|
||||
"publicURL": AUTH_URL + "/v2.0",
|
||||
"region": "RegionOne"
|
||||
}
|
||||
],
|
||||
"endpoints_links": [],
|
||||
"name": "keystone",
|
||||
"type": "identity"
|
||||
}
|
||||
],
|
||||
"token": {
|
||||
"expires": "2035-01-01T00:00:01Z",
|
||||
"id": AUTH_TOKEN,
|
||||
"issued_at": "2013-01-01T00:00:01.692048",
|
||||
"tenant": {
|
||||
"description": None,
|
||||
"enabled": True,
|
||||
"id": "1234",
|
||||
"name": "testtenant"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"id": "5678",
|
||||
"name": USERNAME,
|
||||
"roles": [
|
||||
{
|
||||
"name": "testrole"
|
||||
},
|
||||
],
|
||||
"roles_links": [],
|
||||
"username": USERNAME
|
||||
}
|
||||
}
|
||||
}
|
||||
TEST_RESPONSE_DICT_V3 = {
|
||||
"token": {
|
||||
"audit_ids": [
|
||||
"a"
|
||||
],
|
||||
"catalog": [
|
||||
],
|
||||
"expires_at": "2034-09-29T18:27:15.978064Z",
|
||||
"extras": {},
|
||||
"issued_at": "2014-09-29T17:27:15.978097Z",
|
||||
"methods": [
|
||||
"password"
|
||||
],
|
||||
"project": {
|
||||
"domain": {
|
||||
"id": "default",
|
||||
"name": "Default"
|
||||
},
|
||||
"id": "bbb",
|
||||
"name": "project"
|
||||
},
|
||||
"roles": [
|
||||
],
|
||||
"user": {
|
||||
"domain": {
|
||||
"id": "default",
|
||||
"name": "Default"
|
||||
},
|
||||
"id": "aaa",
|
||||
"name": USERNAME
|
||||
}
|
||||
}
|
||||
}
|
||||
TEST_VERSIONS = {
|
||||
"versions": {
|
||||
"values": [
|
||||
{
|
||||
"id": "v3.0",
|
||||
"links": [
|
||||
{
|
||||
"href": AUTH_URL,
|
||||
"rel": "self"
|
||||
}
|
||||
],
|
||||
"media-types": [
|
||||
{
|
||||
"base": "application/json",
|
||||
"type": "application/vnd.openstack.identity-v3+json"
|
||||
},
|
||||
{
|
||||
"base": "application/xml",
|
||||
"type": "application/vnd.openstack.identity-v3+xml"
|
||||
}
|
||||
],
|
||||
"status": "stable",
|
||||
"updated": "2013-03-06T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": "v2.0",
|
||||
"links": [
|
||||
{
|
||||
"href": AUTH_URL,
|
||||
"rel": "self"
|
||||
},
|
||||
{
|
||||
"href": "http://docs.openstack.org/",
|
||||
"rel": "describedby",
|
||||
"type": "text/html"
|
||||
}
|
||||
],
|
||||
"media-types": [
|
||||
{
|
||||
"base": "application/json",
|
||||
"type": "application/vnd.openstack.identity-v2.0+json"
|
||||
},
|
||||
{
|
||||
"base": "application/xml",
|
||||
"type": "application/vnd.openstack.identity-v2.0+xml"
|
||||
}
|
||||
],
|
||||
"status": "stable",
|
||||
"updated": "2014-04-17T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class FakeStdout:
|
||||
def __init__(self):
|
||||
self.content = []
|
||||
|
||||
def write(self, text):
|
||||
self.content.append(text)
|
||||
|
||||
def make_string(self):
|
||||
result = ''
|
||||
for line in self.content:
|
||||
result = result + line
|
||||
return result
|
||||
|
||||
|
||||
class FakeApp(object):
|
||||
def __init__(self, _stdout):
|
||||
self.stdout = _stdout
|
||||
self.client_manager = None
|
||||
self.stdin = sys.stdin
|
||||
self.stdout = _stdout or sys.stdout
|
||||
self.stderr = sys.stderr
|
||||
|
||||
|
||||
class FakeClient(object):
|
||||
def __init__(self, **kwargs):
|
||||
self.endpoint = kwargs['endpoint']
|
||||
self.token = kwargs['token']
|
||||
|
||||
|
||||
class FakeClientManager(object):
|
||||
def __init__(self):
|
||||
self.compute = None
|
||||
self.identity = None
|
||||
self.image = None
|
||||
self.object_store = None
|
||||
self.volume = None
|
||||
self.network = None
|
||||
self.session = None
|
||||
self.auth_ref = None
|
||||
|
||||
|
||||
class FakeModule(object):
|
||||
def __init__(self, name, version):
|
||||
self.name = name
|
||||
self.__version__ = version
|
||||
|
||||
|
||||
class FakeResource(object):
|
||||
def __init__(self, manager, info, loaded=False):
|
||||
self.manager = manager
|
||||
self._info = info
|
||||
self._add_details(info)
|
||||
self._loaded = loaded
|
||||
|
||||
def _add_details(self, info):
|
||||
for (k, v) in six.iteritems(info):
|
||||
setattr(self, k, v)
|
||||
|
||||
def __repr__(self):
|
||||
reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and
|
||||
k != 'manager')
|
||||
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
|
||||
return "<%s %s>" % (self.__class__.__name__, info)
|
||||
|
||||
|
||||
class FakeResponse(requests.Response):
|
||||
def __init__(self, headers={}, status_code=200, data=None, encoding=None):
|
||||
super(FakeResponse, self).__init__()
|
||||
|
||||
self.status_code = status_code
|
||||
|
||||
self.headers.update(headers)
|
||||
self._content = json.dumps(data)
|
||||
if not isinstance(self._content, six.binary_type):
|
||||
self._content = self._content.encode()
|
@ -0,0 +1,93 @@
|
||||
# Copyright 2012-2013 OpenStack Foundation
|
||||
# Copyright 2013 Nebula 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
|
||||
|
||||
import fixtures
|
||||
import sys
|
||||
import testtools
|
||||
|
||||
from heatclient.tests.unit.osc import fakes
|
||||
|
||||
|
||||
class TestCase(testtools.TestCase):
|
||||
def setUp(self):
|
||||
testtools.TestCase.setUp(self)
|
||||
|
||||
if (os.environ.get("OS_STDOUT_CAPTURE") == "True" or
|
||||
os.environ.get("OS_STDOUT_CAPTURE") == "1"):
|
||||
stdout = self.useFixture(fixtures.StringStream("stdout")).stream
|
||||
self.useFixture(fixtures.MonkeyPatch("sys.stdout", stdout))
|
||||
|
||||
if (os.environ.get("OS_STDERR_CAPTURE") == "True" or
|
||||
os.environ.get("OS_STDERR_CAPTURE") == "1"):
|
||||
stderr = self.useFixture(fixtures.StringStream("stderr")).stream
|
||||
self.useFixture(fixtures.MonkeyPatch("sys.stderr", stderr))
|
||||
|
||||
def assertNotCalled(self, m, msg=None):
|
||||
"""Assert a function was not called"""
|
||||
|
||||
if m.called:
|
||||
if not msg:
|
||||
msg = 'method %s should not have been called' % m
|
||||
self.fail(msg)
|
||||
|
||||
# 2.6 doesn't have the assert dict equals so make sure that it exists
|
||||
if tuple(sys.version_info)[0:2] < (2, 7):
|
||||
|
||||
def assertIsInstance(self, obj, cls, msg=None):
|
||||
"""self.assertTrue(isinstance(obj, cls)), with a nicer message"""
|
||||
|
||||
if not isinstance(obj, cls):
|
||||
standardMsg = '%s is not an instance of %r' % (obj, cls)
|
||||
self.fail(self._formatMessage(msg, standardMsg))
|
||||
|
||||
def assertDictEqual(self, d1, d2, msg=None):
|
||||
# Simple version taken from 2.7
|
||||
self.assertIsInstance(d1, dict,
|
||||
'First argument is not a dictionary')
|
||||
self.assertIsInstance(d2, dict,
|
||||
'Second argument is not a dictionary')
|
||||
if d1 != d2:
|
||||
if msg:
|
||||
self.fail(msg)
|
||||
else:
|
||||
standardMsg = '%r != %r' % (d1, d2)
|
||||
self.fail(standardMsg)
|
||||
|
||||
|
||||
class TestCommand(TestCase):
|
||||
"""Test cliff command classes"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestCommand, self).setUp()
|
||||
# Build up a fake app
|
||||
self.fake_stdout = fakes.FakeStdout()
|
||||
self.app = fakes.FakeApp(self.fake_stdout)
|
||||
self.app.client_manager = fakes.FakeClientManager()
|
||||
|
||||
def check_parser(self, cmd, args, verify_args):
|
||||
cmd_parser = cmd.get_parser('check_parser')
|
||||
try:
|
||||
parsed_args = cmd_parser.parse_args(args)
|
||||
except SystemExit:
|
||||
raise Exception("Argument parse failed")
|
||||
for av in verify_args:
|
||||
attr, value = av
|
||||
if attr:
|
||||
self.assertIn(attr, parsed_args)
|
||||
self.assertEqual(getattr(parsed_args, attr), value)
|
||||
return parsed_args
|
@ -0,0 +1,45 @@
|
||||
# Copyright 2014 OpenStack Foundation
|
||||
#
|
||||
# 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 heatclient.tests.unit.osc import fakes
|
||||
from heatclient.tests.unit.osc import utils
|
||||
|
||||
|
||||
class FakeOrchestrationv1Client(object):
|
||||
def __init__(self, **kwargs):
|
||||
self.http_client = mock.Mock()
|
||||
self.http_client.auth_token = kwargs['token']
|
||||
self.http_client.management_url = kwargs['endpoint']
|
||||
self.stacks = mock.Mock()
|
||||
self.stacks.list = mock.Mock(return_value=[])
|
||||
self.resources = fakes.FakeResource(None, {})
|
||||
self.resource_types = fakes.FakeResource(None, {})
|
||||
self.events = fakes.FakeResource(None, {})
|
||||
self.actions = fakes.FakeResource(None, {})
|
||||
self.build_info = fakes.FakeResource(None, {})
|
||||
self.software_deployments = fakes.FakeResource(None, {})
|
||||
self.software_configs = fakes.FakeResource(None, {})
|
||||
|
||||
|
||||
class TestOrchestrationv1(utils.TestCommand):
|
||||
def setUp(self):
|
||||
super(TestOrchestrationv1, self).setUp()
|
||||
|
||||
self.app.client_manager.orchestration = FakeOrchestrationv1Client(
|
||||
endpoint=fakes.AUTH_URL,
|
||||
token=fakes.AUTH_TOKEN,
|
||||
)
|
@ -0,0 +1,83 @@
|
||||
# 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 testscenarios
|
||||
|
||||
from heatclient.osc.v1 import stack
|
||||
from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes
|
||||
from heatclient.v1 import stacks
|
||||
|
||||
load_tests = testscenarios.load_tests_apply_scenarios
|
||||
|
||||
|
||||
class TestStack(orchestration_fakes.TestOrchestrationv1):
|
||||
def setUp(self):
|
||||
super(TestStack, self).setUp()
|
||||
self.mock_client = self.app.client_manager.orchestration
|
||||
self.stack_client = self.app.client_manager.orchestration.stacks
|
||||
|
||||
|
||||
class TestStackShow(TestStack):
|
||||
|
||||
scenarios = [
|
||||
('table', dict(
|
||||
format='table')),
|
||||
('shell', dict(
|
||||
format='shell')),
|
||||
('value', dict(
|
||||
format='value')),
|
||||
]
|
||||
|
||||
get_response = {"stack": {
|
||||
"disable_rollback": True,
|
||||
"description": "This is a\ndescription\n",
|
||||
"parent": None,
|
||||
"tags": None,
|
||||
"stack_name": "a",
|
||||
"stack_user_project_id": "02ad9bd403d44ff9ba128cf9ce77f989",
|
||||
"stack_status_reason": "Stack UPDATE completed successfully",
|
||||
"creation_time": "2015-08-04T04:46:10",
|
||||
"links": [{
|
||||
"href": "http://192.0.2.1:8004/v1/5dcd28/stacks/a/4af43781",
|
||||
"rel": "self"
|
||||
}],
|
||||
"capabilities": [],
|
||||
"notification_topics": [],
|
||||
"updated_time": "2015-08-05T21:33:28",
|
||||
"timeout_mins": None,
|
||||
"stack_status": "UPDATE_COMPLETE",
|
||||
"stack_owner": None,
|
||||
"parameters": {
|
||||
"OS::project_id": "e0e5e140c5854c259a852621b65dcd28",
|
||||
"OS::stack_id": "4af43781",
|
||||
"OS::stack_name": "a"
|
||||
},
|
||||
"id": "4af43781",
|
||||
"outputs": [],
|
||||
"template_description": "This is a\ndescription\n"}
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(TestStackShow, self).setUp()
|
||||
self.cmd = stack.ShowStack(self.app, None)
|
||||
|
||||
def test_stack_show(self):
|
||||
arglist = ['--format', self.format, 'my_stack']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.stack_client.get = mock.Mock(
|
||||
return_value=stacks.Stack(None, self.get_response))
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.stack_client.get.assert_called_with(**{
|
||||
'stack_id': 'my_stack',
|
||||
})
|
Loading…
Reference in New Issue