Skip to content

Commit 29129a7

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "image: Add 'image import' command"
2 parents 8248efa + 4eea340 commit 29129a7

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
@@ -1557,3 +1558,245 @@ def take_action(self, parsed_args):
15571558
kwargs['data'] = fp
15581559

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