diff --git a/doc/source/command-objects/image.rst b/doc/source/command-objects/image.rst
index 257414242d..8e44f517b5 100644
--- a/doc/source/command-objects/image.rst
+++ b/doc/source/command-objects/image.rst
@@ -7,7 +7,7 @@ Image v1, v2
 image create
 ------------
 
-*Only supported for Image v1*
+*Image v1, v2*
 
 Create/upload an image
 
@@ -32,6 +32,7 @@ Create/upload an image
         [--protected | --unprotected]
         [--public | --private]
         [--property <key=value> [...] ]
+        [--tag <tag> [...] ]
         <image-name>
 
 .. option:: --id <id>
@@ -42,6 +43,8 @@ Create/upload an image
 
     Upload image to this store
 
+    *Image version 1 only.*
+
 .. option:: --container-format <container-format>
 
     Image container format (default: bare)
@@ -54,10 +57,14 @@ Create/upload an image
 
     Image owner project name or ID
 
+    *Image version 1 only.*
+
 .. option:: --size <size>
 
     Image size, in bytes (only used with --location and --copy-from)
 
+    *Image version 1 only.*
+
 .. option:: --min-disk <disk-gb>
 
     Minimum disk size needed to boot image, in gigabytes
@@ -70,10 +77,14 @@ Create/upload an image
 
     Download image from an existing URL
 
+    *Image version 1 only.*
+
 .. option:: --copy-from <image-url>
 
     Copy image from the data store (similar to --location)
 
+    *Image version 1 only.*
+
 .. option:: --file <file>
 
     Upload image from local file
@@ -90,6 +101,8 @@ Create/upload an image
 
     Image hash used for verification
 
+    *Image version 1 only.*
+
 .. option:: --protected
 
     Prevent image from being deleted
@@ -110,6 +123,12 @@ Create/upload an image
 
     Set a property on this image (repeat for multiple values)
 
+.. option:: --tag <tag>
+
+    Set a tag on this image (repeat for multiple values)
+
+    .. versionadded:: 2
+
 .. describe:: <image-name>
 
     New image name
diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py
index 4c019db64a..67390118e7 100644
--- a/openstackclient/image/v2/image.py
+++ b/openstackclient/image/v2/image.py
@@ -22,14 +22,19 @@ import six
 from cliff import command
 from cliff import lister
 from cliff import show
-
 from glanceclient.common import utils as gc_utils
+
 from openstackclient.api import utils as api_utils
+from openstackclient.common import exceptions
 from openstackclient.common import parseractions
 from openstackclient.common import utils
 from openstackclient.identity import common
 
 
+DEFAULT_CONTAINER_FORMAT = 'bare'
+DEFAULT_DISK_FORMAT = 'raw'
+
+
 class AddProjectToImage(show.ShowOne):
     """Associate project with image"""
 
@@ -72,6 +77,186 @@ class AddProjectToImage(show.ShowOne):
         return zip(*sorted(six.iteritems(image_member._info)))
 
 
+class CreateImage(show.ShowOne):
+    """Create/upload an image"""
+
+    log = logging.getLogger(__name__ + ".CreateImage")
+    deadopts = ('owner', 'size', 'location', 'copy-from', 'checksum', 'store')
+
+    def get_parser(self, prog_name):
+        parser = super(CreateImage, self).get_parser(prog_name)
+        # TODO(mordred): add --volume and --force parameters and support
+        # TODO(bunting): There are additional arguments that v1 supported
+        # that v2 either doesn't support or supports weirdly.
+        # --checksum - could be faked clientside perhaps?
+        # --owner - could be set as an update after the put?
+        # --location - maybe location add?
+        # --size - passing image size is actually broken in python-glanceclient
+        # --copy-from - does not exist in v2
+        # --store - does not exits in v2
+        parser.add_argument(
+            "name",
+            metavar="<image-name>",
+            help="New image name",
+        )
+        parser.add_argument(
+            "--id",
+            metavar="<id>",
+            help="Image ID to reserve",
+        )
+        parser.add_argument(
+            "--container-format",
+            default=DEFAULT_CONTAINER_FORMAT,
+            metavar="<container-format>",
+            help="Image container format "
+                 "(default: %s)" % DEFAULT_CONTAINER_FORMAT,
+        )
+        parser.add_argument(
+            "--disk-format",
+            default=DEFAULT_DISK_FORMAT,
+            metavar="<disk-format>",
+            help="Image disk format "
+                 "(default: %s)" % DEFAULT_DISK_FORMAT,
+        )
+        parser.add_argument(
+            "--min-disk",
+            metavar="<disk-gb>",
+            type=int,
+            help="Minimum disk size needed to boot image, in gigabytes",
+        )
+        parser.add_argument(
+            "--min-ram",
+            metavar="<ram-mb>",
+            type=int,
+            help="Minimum RAM size needed to boot image, in megabytes",
+        )
+        parser.add_argument(
+            "--file",
+            metavar="<file>",
+            help="Upload image from local file",
+        )
+        protected_group = parser.add_mutually_exclusive_group()
+        protected_group.add_argument(
+            "--protected",
+            action="store_true",
+            help="Prevent image from being deleted",
+        )
+        protected_group.add_argument(
+            "--unprotected",
+            action="store_true",
+            help="Allow image to be deleted (default)",
+        )
+        public_group = parser.add_mutually_exclusive_group()
+        public_group.add_argument(
+            "--public",
+            action="store_true",
+            help="Image is accessible to the public",
+        )
+        public_group.add_argument(
+            "--private",
+            action="store_true",
+            help="Image is inaccessible to the public (default)",
+        )
+        parser.add_argument(
+            "--property",
+            dest="properties",
+            metavar="<key=value>",
+            action=parseractions.KeyValueAction,
+            help="Set a property on this image "
+                 "(repeat option to set multiple properties)",
+        )
+        parser.add_argument(
+            "--tag",
+            dest="tags",
+            metavar="<tag>",
+            action='append',
+            help="Set a tag on this image "
+                 "(repeat option to set multiple tags)",
+        )
+        for deadopt in self.deadopts:
+            parser.add_argument(
+                "--%s" % deadopt,
+                metavar="<%s>" % deadopt,
+                dest=deadopt.replace('-', '_'),
+                help=argparse.SUPPRESS
+            )
+        return parser
+
+    def take_action(self, parsed_args):
+        self.log.debug("take_action(%s)", parsed_args)
+        image_client = self.app.client_manager.image
+
+        for deadopt in self.deadopts:
+            if getattr(parsed_args, deadopt.replace('-', '_'), None):
+                raise exceptions.CommandError(
+                    "ERROR: --%s was given, which is an Image v1 option"
+                    " that is no longer supported in Image v2" % deadopt)
+
+        # Build an attribute dict from the parsed args, only include
+        # attributes that were actually set on the command line
+        kwargs = {}
+        copy_attrs = ('name', 'id',
+                      'container_format', 'disk_format',
+                      'min_disk', 'min_ram',
+                      'tags')
+        for attr in copy_attrs:
+            if attr in parsed_args:
+                val = getattr(parsed_args, attr, None)
+                if val:
+                    # Only include a value in kwargs for attributes that
+                    # are actually present on the command line
+                    kwargs[attr] = val
+        # properties should get flattened into the general kwargs
+        if getattr(parsed_args, 'properties', None):
+            for k, v in six.iteritems(parsed_args.properties):
+                kwargs[k] = str(v)
+        # Handle exclusive booleans with care
+        # Avoid including attributes in kwargs if an option is not
+        # present on the command line.  These exclusive booleans are not
+        # a single value for the pair of options because the default must be
+        # to do nothing when no options are present as opposed to always
+        # setting a default.
+        if parsed_args.protected:
+            kwargs['protected'] = True
+        if parsed_args.unprotected:
+            kwargs['protected'] = False
+        if parsed_args.public:
+            kwargs['visibility'] = 'public'
+        if parsed_args.private:
+            kwargs['visibility'] = 'private'
+
+        # open the file first to ensure any failures are handled before the
+        # image is created
+        fp = gc_utils.get_data_file(parsed_args)
+
+        if fp is None and parsed_args.file:
+            self.log.warning("Failed to get an image file.")
+            return {}, {}
+
+        image = image_client.images.create(**kwargs)
+
+        if fp is not None:
+            with fp:
+                try:
+                    image_client.images.upload(image.id, fp)
+                except Exception as e:
+                    # If the upload fails for some reason attempt to remove the
+                    # dangling queued image made by the create() call above but
+                    # only if the user did not specify an id which indicates
+                    # the Image already exists and should be left alone.
+                    try:
+                        if 'id' not in kwargs:
+                            image_client.images.delete(image.id)
+                    except Exception:
+                        pass  # we don't care about this one
+                    raise e  # now, throw the upload exception again
+
+                # update the image after the data has been uploaded
+                image = image_client.images.get(image.id)
+
+        return zip(*sorted(six.iteritems(image)))
+
+
 class DeleteImage(command.Command):
     """Delete image(s)"""
 
diff --git a/openstackclient/tests/image/v2/test_image.py b/openstackclient/tests/image/v2/test_image.py
index bfb9476518..bb720d79c8 100644
--- a/openstackclient/tests/image/v2/test_image.py
+++ b/openstackclient/tests/image/v2/test_image.py
@@ -19,6 +19,7 @@ import mock
 import warlock
 
 from glanceclient.v2 import schemas
+from openstackclient.common import exceptions
 from openstackclient.image.v2 import image
 from openstackclient.tests import fakes
 from openstackclient.tests.identity.v3 import fakes as identity_fakes
@@ -41,6 +42,191 @@ class TestImage(image_fakes.TestImagev2):
         self.domain_mock.reset_mock()
 
 
+class TestImageCreate(TestImage):
+
+    def setUp(self):
+        super(TestImageCreate, self).setUp()
+
+        self.images_mock.create.return_value = fakes.FakeResource(
+            None,
+            copy.deepcopy(image_fakes.IMAGE),
+            loaded=True,
+        )
+        # This is the return value for utils.find_resource()
+        self.images_mock.get.return_value = copy.deepcopy(image_fakes.IMAGE)
+        self.images_mock.update.return_value = fakes.FakeResource(
+            None,
+            copy.deepcopy(image_fakes.IMAGE),
+            loaded=True,
+        )
+
+        # Get the command object to test
+        self.cmd = image.CreateImage(self.app, None)
+
+    def test_image_reserve_no_options(self):
+        mock_exception = {
+            'find.side_effect': exceptions.CommandError('x'),
+        }
+        self.images_mock.configure_mock(**mock_exception)
+        arglist = [
+            image_fakes.image_name,
+        ]
+        verifylist = [
+            ('container_format', image.DEFAULT_CONTAINER_FORMAT),
+            ('disk_format', image.DEFAULT_DISK_FORMAT),
+            ('name', image_fakes.image_name),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        # DisplayCommandBase.take_action() returns two tuples
+        columns, data = self.cmd.take_action(parsed_args)
+
+        # ImageManager.create(name=, **)
+        self.images_mock.create.assert_called_with(
+            name=image_fakes.image_name,
+            container_format=image.DEFAULT_CONTAINER_FORMAT,
+            disk_format=image.DEFAULT_DISK_FORMAT,
+        )
+
+        # Verify update() was not called, if it was show the args
+        self.assertEqual(self.images_mock.update.call_args_list, [])
+
+        self.images_mock.upload.assert_called_with(
+            mock.ANY, mock.ANY,
+        )
+
+        self.assertEqual(image_fakes.IMAGE_columns, columns)
+        self.assertEqual(image_fakes.IMAGE_data, data)
+
+    @mock.patch('glanceclient.common.utils.get_data_file', name='Open')
+    def test_image_reserve_options(self, mock_open):
+        mock_file = mock.MagicMock(name='File')
+        mock_open.return_value = mock_file
+        mock_open.read.return_value = None
+        mock_exception = {
+            'find.side_effect': exceptions.CommandError('x'),
+        }
+        self.images_mock.configure_mock(**mock_exception)
+        arglist = [
+            '--container-format', 'ovf',
+            '--disk-format', 'fs',
+            '--min-disk', '10',
+            '--min-ram', '4',
+            '--protected',
+            '--private',
+            image_fakes.image_name,
+        ]
+        verifylist = [
+            ('container_format', 'ovf'),
+            ('disk_format', 'fs'),
+            ('min_disk', 10),
+            ('min_ram', 4),
+            ('protected', True),
+            ('unprotected', False),
+            ('public', False),
+            ('private', True),
+            ('name', image_fakes.image_name),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        # DisplayCommandBase.take_action() returns two tuples
+        columns, data = self.cmd.take_action(parsed_args)
+
+        # ImageManager.create(name=, **)
+        self.images_mock.create.assert_called_with(
+            name=image_fakes.image_name,
+            container_format='ovf',
+            disk_format='fs',
+            min_disk=10,
+            min_ram=4,
+            protected=True,
+            visibility='private',
+        )
+
+        # Verify update() was not called, if it was show the args
+        self.assertEqual(self.images_mock.update.call_args_list, [])
+
+        self.images_mock.upload.assert_called_with(
+            mock.ANY, mock.ANY,
+        )
+
+        self.assertEqual(image_fakes.IMAGE_columns, columns)
+        self.assertEqual(image_fakes.IMAGE_data, data)
+
+    @mock.patch('glanceclient.common.utils.get_data_file', name='Open')
+    def test_image_create_file(self, mock_open):
+        mock_file = mock.MagicMock(name='File')
+        mock_open.return_value = mock_file
+        mock_open.read.return_value = image_fakes.IMAGE_data
+        mock_exception = {
+            'find.side_effect': exceptions.CommandError('x'),
+        }
+        self.images_mock.configure_mock(**mock_exception)
+
+        arglist = [
+            '--file', 'filer',
+            '--unprotected',
+            '--public',
+            '--property', 'Alpha=1',
+            '--property', 'Beta=2',
+            '--tag', 'awesome',
+            '--tag', 'better',
+            image_fakes.image_name,
+        ]
+        verifylist = [
+            ('file', 'filer'),
+            ('protected', False),
+            ('unprotected', True),
+            ('public', True),
+            ('private', False),
+            ('properties', {'Alpha': '1', 'Beta': '2'}),
+            ('tags', ['awesome', 'better']),
+            ('name', image_fakes.image_name),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        # DisplayCommandBase.take_action() returns two tuples
+        columns, data = self.cmd.take_action(parsed_args)
+
+        # ImageManager.create(name=, **)
+        self.images_mock.create.assert_called_with(
+            name=image_fakes.image_name,
+            container_format=image.DEFAULT_CONTAINER_FORMAT,
+            disk_format=image.DEFAULT_DISK_FORMAT,
+            protected=False,
+            visibility='public',
+            Alpha='1',
+            Beta='2',
+            tags=['awesome', 'better'],
+        )
+
+        # Verify update() was not called, if it was show the args
+        self.assertEqual(self.images_mock.update.call_args_list, [])
+
+        self.images_mock.upload.assert_called_with(
+            mock.ANY, mock.ANY,
+        )
+
+        self.assertEqual(image_fakes.IMAGE_columns, columns)
+        self.assertEqual(image_fakes.IMAGE_data, data)
+
+    def test_image_dead_options(self):
+
+        arglist = [
+            '--owner', 'nobody',
+            image_fakes.image_name,
+        ]
+        verifylist = [
+            ('owner', 'nobody'),
+            ('name', image_fakes.image_name),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        self.assertRaises(
+            exceptions.CommandError,
+            self.cmd.take_action, parsed_args)
+
+
 class TestAddProjectToImage(TestImage):
 
     def setUp(self):
diff --git a/setup.cfg b/setup.cfg
index f2f4833b2f..0b9368254b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -316,6 +316,7 @@ openstack.image.v1 =
 
 openstack.image.v2 =
     image_add_project = openstackclient.image.v2.image:AddProjectToImage
+    image_create = openstackclient.image.v2.image:CreateImage
     image_delete = openstackclient.image.v2.image:DeleteImage
     image_list = openstackclient.image.v2.image:ListImage
     image_remove_project = openstackclient.image.v2.image:RemoveProjectImage