diff --git a/openstackclient/common/progressbar.py b/openstackclient/common/progressbar.py new file mode 100644 index 0000000000..ef767a9cf8 --- /dev/null +++ b/openstackclient/common/progressbar.py @@ -0,0 +1,67 @@ +# Copyright 2013 OpenStack Foundation +# All Rights Reserved. +# +# 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 sys + + +class _ProgressBarBase(object): + """A progress bar provider for a wrapped obect. + + Base abstract class used by specific class wrapper to show + a progress bar when the wrapped object are consumed. + + :param wrapped: Object to wrap that hold data to be consumed. + :param totalsize: The total size of the data in the wrapped object. + + :note: The progress will be displayed only if sys.stdout is a tty. + """ + + def __init__(self, wrapped, totalsize): + self._wrapped = wrapped + self._totalsize = float(totalsize) + self._show_progress = sys.stdout.isatty() and self._totalsize != 0 + self._percent = 0 + + def _display_progress_bar(self, size_read): + if self._show_progress: + self._percent += size_read / self._totalsize + # Output something like this: [==========> ] 49% + sys.stdout.write('\r[{0:<30}] {1:.0%}'.format( + '=' * int(round(self._percent * 29)) + '>', self._percent + )) + sys.stdout.flush() + + def __getattr__(self, attr): + # Forward other attribute access to the wrapped object. + return getattr(self._wrapped, attr) + + +class VerboseFileWrapper(_ProgressBarBase): + """A file wrapper with a progress bar. + + The file wrapper shows and advances a progress bar whenever the + wrapped file's read method is called. + """ + + def read(self, *args, **kwargs): + data = self._wrapped.read(*args, **kwargs) + if data: + self._display_progress_bar(len(data)) + else: + if self._show_progress: + # Break to a new line from the progress bar for incoming + # output. + sys.stdout.write('\n') + return data diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index fa7f4be562..7177929339 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -30,6 +30,7 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient.common import progressbar from openstackclient.common import sdk_utils from openstackclient.i18n import _ from openstackclient.identity import common @@ -255,6 +256,12 @@ class CreateImage(command.ShowOne): help=_("Force image creation if volume is in use " "(only meaningful with --volume)"), ) + parser.add_argument( + "--progress", + action="store_true", + default=False, + help=_("Show upload progress bar."), + ) parser.add_argument( '--sign-key-path', metavar="", @@ -412,6 +419,11 @@ class CreateImage(command.ShowOne): if fp is None and parsed_args.file: LOG.warning(_("Failed to get an image file.")) return {}, {} + if fp is not None and parsed_args.progress: + filesize = os.path.getsize(fname) + if filesize is not None: + kwargs['validate_checksum'] = False + kwargs['data'] = progressbar.VerboseFileWrapper(fp, filesize) elif fname: kwargs['filename'] = fname elif fp: diff --git a/openstackclient/tests/unit/common/test_progressbar.py b/openstackclient/tests/unit/common/test_progressbar.py new file mode 100644 index 0000000000..7bc0b6baf4 --- /dev/null +++ b/openstackclient/tests/unit/common/test_progressbar.py @@ -0,0 +1,77 @@ +# 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 sys + +import six + +from openstackclient.common import progressbar +from openstackclient.tests.unit import utils + + +class TestProgressBarWrapper(utils.TestCase): + + def test_iter_file_display_progress_bar(self): + size = 98304 + file_obj = six.StringIO('X' * size) + saved_stdout = sys.stdout + try: + sys.stdout = output = FakeTTYStdout() + file_obj = progressbar.VerboseFileWrapper(file_obj, size) + chunksize = 1024 + chunk = file_obj.read(chunksize) + while chunk: + chunk = file_obj.read(chunksize) + self.assertEqual( + '[%s>] 100%%\n' % ('=' * 29), + output.getvalue() + ) + finally: + sys.stdout = saved_stdout + + def test_iter_file_no_tty(self): + size = 98304 + file_obj = six.StringIO('X' * size) + saved_stdout = sys.stdout + try: + sys.stdout = output = FakeNoTTYStdout() + file_obj = progressbar.VerboseFileWrapper(file_obj, size) + chunksize = 1024 + chunk = file_obj.read(chunksize) + while chunk: + chunk = file_obj.read(chunksize) + # If stdout is not a tty progress bar should do nothing. + self.assertEqual('', output.getvalue()) + finally: + sys.stdout = saved_stdout + + +class FakeTTYStdout(six.StringIO): + """A Fake stdout that try to emulate a TTY device as much as possible.""" + + def isatty(self): + return True + + def write(self, data): + # When a CR (carriage return) is found reset file. + if data.startswith('\r'): + self.seek(0) + data = data[1:] + return six.StringIO.write(self, data) + + +class FakeNoTTYStdout(FakeTTYStdout): + """A Fake stdout that is not a TTY device.""" + + def isatty(self): + return False diff --git a/releasenotes/notes/add-image-progress-option-to-create-1ed1881d58ebad4b.yaml b/releasenotes/notes/add-image-progress-option-to-create-1ed1881d58ebad4b.yaml new file mode 100644 index 0000000000..593c5bd3b8 --- /dev/null +++ b/releasenotes/notes/add-image-progress-option-to-create-1ed1881d58ebad4b.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``--progress`` option to ``image create`` command to enable a progress + bar when creating and uploading an image.