Skip to content

Commit 4eea340

Browse files
committed
image: Add 'image import' command
Note that we require some additional functionality in SDK for this to work properly, but it's a start. Change-Id: I87f94db6cced67f36f71685e791416f9eed16bd0 Signed-off-by: Stephen Finucane <sfinucan@redhat.com>
1 parent 1fb8d1f commit 4eea340

5 files changed

Lines changed: 473 additions & 6 deletions

File tree

doc/source/cli/data/glance.csv

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ image-create-via-import,,EXPERIMENTAL: Create a new image via image import.
44
image-deactivate,image set --deactivate,Deactivate specified image.
55
image-delete,image delete,Delete specified image.
66
image-download,image save,Download a specific image.
7-
image-import,,Initiate the image import taskflow.
7+
image-import,image import,Initiate the image import taskflow.
88
image-list,image list,List images you can access.
99
image-reactivate,image set --activate,Reactivate specified image.
1010
image-show,image show,Describe a specific image.

openstackclient/image/v2/image.py

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import sys
2323

2424
from cinderclient import api_versions
25+
from openstack import exceptions as sdk_exceptions
2526
from openstack.image import image_signer
2627
from osc_lib.api import utils as api_utils
2728
from osc_lib.cli import format_columns
@@ -1561,3 +1562,245 @@ def take_action(self, parsed_args):
15611562
kwargs['data'] = fp
15621563

15631564
image_client.stage_image(image, **kwargs)
1565+
1566+
1567+
class ImportImage(command.ShowOne):
1568+
_description = _(
1569+
"Initiate the image import process.\n"
1570+
"This requires support for the interoperable image import process, "
1571+
"which was first introduced in Image API version 2.6 "
1572+
"(Glance 16.0.0 (Queens))"
1573+
)
1574+
1575+
def get_parser(self, prog_name):
1576+
parser = super().get_parser(prog_name)
1577+
1578+
parser.add_argument(
1579+
'image',
1580+
metavar='<image>',
1581+
help=_('Image to initiate import process for (name or ID)'),
1582+
)
1583+
# TODO(stephenfin): Uncomment help text when we have this command
1584+
# implemented
1585+
parser.add_argument(
1586+
'--method',
1587+
metavar='<method>',
1588+
default='glance-direct',
1589+
dest='import_method',
1590+
choices=[
1591+
'glance-direct',
1592+
'web-download',
1593+
'glance-download',
1594+
'copy-image',
1595+
],
1596+
help=_(
1597+
"Import method used for image import process. "
1598+
"Not all deployments will support all methods. "
1599+
# "Valid values can be retrieved with the 'image import "
1600+
# "methods' command. "
1601+
"The 'glance-direct' method (default) requires images be "
1602+
"first staged using the 'image-stage' command."
1603+
),
1604+
)
1605+
parser.add_argument(
1606+
'--uri',
1607+
metavar='<uri>',
1608+
help=_(
1609+
"URI to download the external image "
1610+
"(only valid with the 'web-download' import method)"
1611+
),
1612+
)
1613+
parser.add_argument(
1614+
'--remote-image',
1615+
metavar='<REMOTE_IMAGE>',
1616+
help=_(
1617+
"The image of remote glance (ID only) to be imported "
1618+
"(only valid with the 'glance-download' import method)"
1619+
),
1620+
)
1621+
parser.add_argument(
1622+
'--remote-region',
1623+
metavar='<REMOTE_GLANCE_REGION>',
1624+
help=_(
1625+
"The remote Glance region to download the image from "
1626+
"(only valid with the 'glance-download' import method)"
1627+
),
1628+
)
1629+
parser.add_argument(
1630+
'--remote-service-interface',
1631+
metavar='<REMOTE_SERVICE_INTERFACE>',
1632+
help=_(
1633+
"The remote Glance service interface to use when importing "
1634+
"images "
1635+
"(only valid with the 'glance-download' import method)"
1636+
),
1637+
)
1638+
stores_group = parser.add_mutually_exclusive_group()
1639+
stores_group.add_argument(
1640+
'--store',
1641+
metavar='<STORE>',
1642+
dest='stores',
1643+
nargs='*',
1644+
help=_(
1645+
"Backend store to upload image to "
1646+
"(specify multiple times to upload to multiple stores) "
1647+
"(either '--store' or '--all-stores' required with the "
1648+
"'copy-image' import method)"
1649+
),
1650+
)
1651+
stores_group.add_argument(
1652+
'--all-stores',
1653+
help=_(
1654+
"Make image available to all stores "
1655+
"(either '--store' or '--all-stores' required with the "
1656+
"'copy-image' import method)"
1657+
),
1658+
)
1659+
parser.add_argument(
1660+
'--allow-failure',
1661+
action='store_true',
1662+
dest='allow_failure',
1663+
default=True,
1664+
help=_(
1665+
'When uploading to multiple stores, indicate that the import '
1666+
'should be continue should any of the uploads fail. '
1667+
'Only usable with --stores or --all-stores'
1668+
),
1669+
)
1670+
parser.add_argument(
1671+
'--disallow-failure',
1672+
action='store_true',
1673+
dest='allow_failure',
1674+
default=True,
1675+
help=_(
1676+
'When uploading to multiple stores, indicate that the import '
1677+
'should be reverted should any of the uploads fail. '
1678+
'Only usable with --stores or --all-stores'
1679+
),
1680+
)
1681+
parser.add_argument(
1682+
'--wait',
1683+
action='store_true',
1684+
help=_('Wait for operation to complete'),
1685+
)
1686+
return parser
1687+
1688+
def take_action(self, parsed_args):
1689+
image_client = self.app.client_manager.image
1690+
1691+
try:
1692+
import_info = image_client.get_import_info()
1693+
except sdk_exceptions.ResourceNotFound:
1694+
msg = _(
1695+
'The Image Import feature is not supported by this deployment'
1696+
)
1697+
raise exceptions.CommandError(msg)
1698+
1699+
import_methods = import_info.import_methods['value']
1700+
1701+
if parsed_args.import_method not in import_methods:
1702+
msg = _(
1703+
"The '%s' import method is not supported by this deployment. "
1704+
"Supported: %s"
1705+
)
1706+
raise exceptions.CommandError(
1707+
msg % (parsed_args.import_method, ', '.join(import_methods)),
1708+
)
1709+
1710+
if parsed_args.import_method == 'web-download':
1711+
if not parsed_args.uri:
1712+
msg = _(
1713+
"The '--uri' option is required when using "
1714+
"'--method=web-download'"
1715+
)
1716+
raise exceptions.CommandError(msg)
1717+
else:
1718+
if parsed_args.uri:
1719+
msg = _(
1720+
"The '--uri' option is only supported when using "
1721+
"'--method=web-download'"
1722+
)
1723+
raise exceptions.CommandError(msg)
1724+
1725+
if parsed_args.import_method == 'glance-download':
1726+
if not (parsed_args.remote_region and parsed_args.remote_image):
1727+
msg = _(
1728+
"The '--remote-region' and '--remote-image' options are "
1729+
"required when using '--method=web-download'"
1730+
)
1731+
raise exceptions.CommandError(msg)
1732+
else:
1733+
if parsed_args.remote_region:
1734+
msg = _(
1735+
"The '--remote-region' option is only supported when "
1736+
"using '--method=glance-download'"
1737+
)
1738+
raise exceptions.CommandError(msg)
1739+
1740+
if parsed_args.remote_image:
1741+
msg = _(
1742+
"The '--remote-image' option is only supported when using "
1743+
"'--method=glance-download'"
1744+
)
1745+
raise exceptions.CommandError(msg)
1746+
1747+
if parsed_args.remote_service_interface:
1748+
msg = _(
1749+
"The '--remote-service-interface' option is only "
1750+
"supported when using '--method=glance-download'"
1751+
)
1752+
raise exceptions.CommandError(msg)
1753+
1754+
if parsed_args.import_method == 'copy-image':
1755+
if not (parsed_args.stores or parsed_args.all_stores):
1756+
msg = _(
1757+
"The '--stores' or '--all-stores' options are required "
1758+
"when using '--method=copy-image'"
1759+
)
1760+
raise exceptions.CommandError(msg)
1761+
1762+
image = image_client.find_image(parsed_args.image)
1763+
1764+
if not image.container_format and not image.disk_format:
1765+
msg = _(
1766+
"The 'container_format' and 'disk_format' properties "
1767+
"must be set on an image before it can be imported"
1768+
)
1769+
raise exceptions.CommandError(msg)
1770+
1771+
if parsed_args.import_method == 'glance-direct':
1772+
if image.status != 'uploading':
1773+
msg = _(
1774+
"The 'glance-direct' import method can only be used with "
1775+
"an image in status 'uploading'"
1776+
)
1777+
raise exceptions.CommandError(msg)
1778+
elif parsed_args.import_method == 'web-download':
1779+
if image.status != 'queued':
1780+
msg = _(
1781+
"The 'web-download' import method can only be used with "
1782+
"an image in status 'queued'"
1783+
)
1784+
raise exceptions.CommandError(msg)
1785+
elif parsed_args.import_method == 'copy-image':
1786+
if image.status != 'active':
1787+
msg = _(
1788+
"The 'copy-image' import method can only be used with "
1789+
"an image in status 'active'"
1790+
)
1791+
raise exceptions.CommandError(msg)
1792+
1793+
image_client.import_image(
1794+
image,
1795+
method=parsed_args.import_method,
1796+
# uri=parsed_args.uri,
1797+
# remote_region=parsed_args.remote_region,
1798+
# remote_image=parsed_args.remote_image,
1799+
# remote_service_interface=parsed_args.remote_service_interface,
1800+
stores=parsed_args.stores,
1801+
all_stores=parsed_args.all_stores,
1802+
all_stores_must_succeed=not parsed_args.allow_failure,
1803+
)
1804+
1805+
info = _format_image(image)
1806+
return zip(*sorted(info.items()))

openstackclient/tests/unit/image/v2/fakes.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from openstack.image.v2 import image
2020
from openstack.image.v2 import member
2121
from openstack.image.v2 import metadef_namespace
22+
from openstack.image.v2 import service_info as _service_info
2223
from openstack.image.v2 import task
2324

2425
from openstackclient.tests.unit import fakes
@@ -39,6 +40,7 @@ def __init__(self, **kwargs):
3940
self.reactivate_image = mock.Mock()
4041
self.deactivate_image = mock.Mock()
4142
self.stage_image = mock.Mock()
43+
self.import_image = mock.Mock()
4244

4345
self.members = mock.Mock()
4446
self.add_member = mock.Mock()
@@ -49,17 +51,15 @@ def __init__(self, **kwargs):
4951
self.metadef_namespaces = mock.Mock()
5052

5153
self.tasks = mock.Mock()
54+
self.tasks.resource_class = fakes.FakeResource(None, {})
5255
self.get_task = mock.Mock()
5356

57+
self.get_import_info = mock.Mock()
58+
5459
self.auth_token = kwargs['token']
5560
self.management_url = kwargs['endpoint']
5661
self.version = 2.0
5762

58-
self.tasks = mock.Mock()
59-
self.tasks.resource_class = fakes.FakeResource(None, {})
60-
61-
self.metadef_namespaces = mock.Mock()
62-
6363

6464
class TestImagev2(utils.TestCommand):
6565

@@ -143,6 +143,33 @@ def create_one_image_member(attrs=None):
143143
return member.Member(**image_member_info)
144144

145145

146+
def create_one_import_info(attrs=None):
147+
"""Create a fake import info.
148+
149+
:param attrs: A dictionary with all attributes of import info
150+
:type attrs: dict
151+
:return: A fake Import object.
152+
:rtype: `openstack.image.v2.service_info.Import`
153+
"""
154+
attrs = attrs or {}
155+
156+
import_info = {
157+
'import-methods': {
158+
'description': 'Import methods available.',
159+
'type': 'array',
160+
'value': [
161+
'glance-direct',
162+
'web-download',
163+
'glance-download',
164+
'copy-image',
165+
]
166+
}
167+
}
168+
import_info.update(attrs)
169+
170+
return _service_info.Import(**import_info)
171+
172+
146173
def create_one_task(attrs=None):
147174
"""Create a fake task.
148175

0 commit comments

Comments
 (0)