Merge "image: Remove _base_proxy module"
This commit is contained in:
@@ -1,321 +0,0 @@
|
|||||||
# 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 abc
|
|
||||||
import os
|
|
||||||
|
|
||||||
from openstack import exceptions
|
|
||||||
from openstack import proxy
|
|
||||||
from openstack import utils
|
|
||||||
|
|
||||||
|
|
||||||
def _get_name_and_filename(name, image_format):
|
|
||||||
# See if name points to an existing file
|
|
||||||
if os.path.exists(name):
|
|
||||||
# Neat. Easy enough
|
|
||||||
return os.path.splitext(os.path.basename(name))[0], name
|
|
||||||
|
|
||||||
# Try appending the disk format
|
|
||||||
name_with_ext = '.'.join((name, image_format))
|
|
||||||
if os.path.exists(name_with_ext):
|
|
||||||
return os.path.basename(name), name_with_ext
|
|
||||||
|
|
||||||
return name, None
|
|
||||||
|
|
||||||
|
|
||||||
class BaseImageProxy(proxy.Proxy, metaclass=abc.ABCMeta):
|
|
||||||
|
|
||||||
retriable_status_codes = [503]
|
|
||||||
|
|
||||||
_IMAGE_MD5_KEY = 'owner_specified.openstack.md5'
|
|
||||||
_IMAGE_SHA256_KEY = 'owner_specified.openstack.sha256'
|
|
||||||
_IMAGE_OBJECT_KEY = 'owner_specified.openstack.object'
|
|
||||||
|
|
||||||
# NOTE(shade) shade keys were owner_specified.shade.md5 - we need to add
|
|
||||||
# those to freshness checks so that a shade->sdk transition
|
|
||||||
# doesn't result in a re-upload
|
|
||||||
_SHADE_IMAGE_MD5_KEY = 'owner_specified.shade.md5'
|
|
||||||
_SHADE_IMAGE_SHA256_KEY = 'owner_specified.shade.sha256'
|
|
||||||
_SHADE_IMAGE_OBJECT_KEY = 'owner_specified.shade.object'
|
|
||||||
|
|
||||||
# ====== IMAGES ======
|
|
||||||
def create_image(
|
|
||||||
self,
|
|
||||||
name,
|
|
||||||
filename=None,
|
|
||||||
container=None,
|
|
||||||
md5=None,
|
|
||||||
sha256=None,
|
|
||||||
disk_format=None,
|
|
||||||
container_format=None,
|
|
||||||
disable_vendor_agent=True,
|
|
||||||
allow_duplicates=False,
|
|
||||||
meta=None,
|
|
||||||
wait=False,
|
|
||||||
timeout=3600,
|
|
||||||
data=None,
|
|
||||||
validate_checksum=False,
|
|
||||||
use_import=False,
|
|
||||||
stores=None,
|
|
||||||
tags=None,
|
|
||||||
all_stores=None,
|
|
||||||
all_stores_must_succeed=None,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
"""Upload an image.
|
|
||||||
|
|
||||||
:param str name: Name of the image to create. If it is a pathname
|
|
||||||
of an image, the name will be constructed from the extensionless
|
|
||||||
basename of the path.
|
|
||||||
:param str filename: The path to the file to upload, if needed.
|
|
||||||
(optional, defaults to None)
|
|
||||||
:param data: Image data (string or file-like object). It is mutually
|
|
||||||
exclusive with filename
|
|
||||||
:param str container: Name of the container in swift where images
|
|
||||||
should be uploaded for import if the cloud requires such a thing.
|
|
||||||
(optional, defaults to 'images')
|
|
||||||
:param str md5: md5 sum of the image file. If not given, an md5 will
|
|
||||||
be calculated.
|
|
||||||
:param str sha256: sha256 sum of the image file. If not given, an md5
|
|
||||||
will be calculated.
|
|
||||||
:param str disk_format: The disk format the image is in. (optional,
|
|
||||||
defaults to the os-client-config config value for this cloud)
|
|
||||||
:param str container_format: The container format the image is in.
|
|
||||||
(optional, defaults to the os-client-config config value for this
|
|
||||||
cloud)
|
|
||||||
:param list tags: List of tags for this image. Each tag is a string
|
|
||||||
of at most 255 chars.
|
|
||||||
:param bool disable_vendor_agent: Whether or not to append metadata
|
|
||||||
flags to the image to inform the cloud in question to not expect a
|
|
||||||
vendor agent to be runing. (optional, defaults to True)
|
|
||||||
:param allow_duplicates: If true, skips checks that enforce unique
|
|
||||||
image name. (optional, defaults to False)
|
|
||||||
:param meta: A dict of key/value pairs to use for metadata that
|
|
||||||
bypasses automatic type conversion.
|
|
||||||
:param bool wait: If true, waits for image to be created. Defaults to
|
|
||||||
true - however, be aware that one of the upload methods is always
|
|
||||||
synchronous.
|
|
||||||
:param timeout: Seconds to wait for image creation. None is forever.
|
|
||||||
:param bool validate_checksum: If true and cloud returns checksum,
|
|
||||||
compares return value with the one calculated or passed into this
|
|
||||||
call. If value does not match - raises exception. Default is
|
|
||||||
'false'
|
|
||||||
:param bool use_import: Use the interoperable image import mechanism
|
|
||||||
to import the image. This defaults to false because it is harder on
|
|
||||||
the target cloud so should only be used when needed, such as when
|
|
||||||
the user needs the cloud to transform image format. If the cloud
|
|
||||||
has disabled direct uploads, this will default to true.
|
|
||||||
:param stores: List of stores to be used when enabled_backends is
|
|
||||||
activated in glance. List values can be the id of a store or a
|
|
||||||
:class:`~openstack.image.v2.service_info.Store` instance.
|
|
||||||
Implies ``use_import`` equals ``True``.
|
|
||||||
:param all_stores: Upload to all available stores. Mutually exclusive
|
|
||||||
with ``store`` and ``stores``.
|
|
||||||
Implies ``use_import`` equals ``True``.
|
|
||||||
:param all_stores_must_succeed: When set to True, if an error occurs
|
|
||||||
during the upload in at least one store, the worfklow fails, the
|
|
||||||
data is deleted from stores where copying is done (not staging),
|
|
||||||
and the state of the image is unchanged. When set to False, the
|
|
||||||
workflow will fail (data deleted from stores, …) only if the import
|
|
||||||
fails on all stores specified by the user. In case of a partial
|
|
||||||
success, the locations added to the image will be the stores where
|
|
||||||
the data has been correctly uploaded.
|
|
||||||
Default is True.
|
|
||||||
Implies ``use_import`` equals ``True``.
|
|
||||||
|
|
||||||
Additional kwargs will be passed to the image creation as additional
|
|
||||||
metadata for the image and will have all values converted to string
|
|
||||||
except for min_disk, min_ram, size and virtual_size which will be
|
|
||||||
converted to int.
|
|
||||||
|
|
||||||
If you are sure you have all of your data types correct or have an
|
|
||||||
advanced need to be explicit, use meta. If you are just a normal
|
|
||||||
consumer, using kwargs is likely the right choice.
|
|
||||||
|
|
||||||
If a value is in meta and kwargs, meta wins.
|
|
||||||
|
|
||||||
:returns: A ``munch.Munch`` of the Image object
|
|
||||||
:raises: SDKException if there are problems uploading
|
|
||||||
"""
|
|
||||||
if container is None:
|
|
||||||
container = self._connection._OBJECT_AUTOCREATE_CONTAINER
|
|
||||||
|
|
||||||
if not meta:
|
|
||||||
meta = {}
|
|
||||||
|
|
||||||
if not disk_format:
|
|
||||||
disk_format = self._connection.config.config['image_format']
|
|
||||||
|
|
||||||
if not container_format:
|
|
||||||
# https://docs.openstack.org/image-guide/image-formats.html
|
|
||||||
container_format = 'bare'
|
|
||||||
|
|
||||||
if data and filename:
|
|
||||||
raise exceptions.SDKException(
|
|
||||||
'Passing filename and data simultaneously is not supported'
|
|
||||||
)
|
|
||||||
|
|
||||||
# If there is no filename, see if name is actually the filename
|
|
||||||
if not filename and not data:
|
|
||||||
name, filename = _get_name_and_filename(
|
|
||||||
name,
|
|
||||||
self._connection.config.config['image_format'],
|
|
||||||
)
|
|
||||||
|
|
||||||
if validate_checksum and data and not isinstance(data, bytes):
|
|
||||||
raise exceptions.SDKException(
|
|
||||||
'Validating checksum is not possible when data is not a '
|
|
||||||
'direct binary object'
|
|
||||||
)
|
|
||||||
|
|
||||||
if not (md5 or sha256) and validate_checksum:
|
|
||||||
if filename:
|
|
||||||
md5, sha256 = utils._get_file_hashes(filename)
|
|
||||||
elif data and isinstance(data, bytes):
|
|
||||||
md5, sha256 = utils._calculate_data_hashes(data)
|
|
||||||
|
|
||||||
if allow_duplicates:
|
|
||||||
current_image = None
|
|
||||||
else:
|
|
||||||
current_image = self.find_image(name)
|
|
||||||
if current_image:
|
|
||||||
# NOTE(pas-ha) 'properties' may be absent or be None
|
|
||||||
props = current_image.get('properties') or {}
|
|
||||||
md5_key = props.get(
|
|
||||||
self._IMAGE_MD5_KEY,
|
|
||||||
props.get(self._SHADE_IMAGE_MD5_KEY, ''),
|
|
||||||
)
|
|
||||||
sha256_key = props.get(
|
|
||||||
self._IMAGE_SHA256_KEY,
|
|
||||||
props.get(self._SHADE_IMAGE_SHA256_KEY, ''),
|
|
||||||
)
|
|
||||||
up_to_date = utils._hashes_up_to_date(
|
|
||||||
md5=md5,
|
|
||||||
sha256=sha256,
|
|
||||||
md5_key=md5_key,
|
|
||||||
sha256_key=sha256_key,
|
|
||||||
)
|
|
||||||
if up_to_date:
|
|
||||||
self.log.debug(
|
|
||||||
"image %(name)s exists and is up to date",
|
|
||||||
{'name': name},
|
|
||||||
)
|
|
||||||
return current_image
|
|
||||||
else:
|
|
||||||
self.log.debug(
|
|
||||||
"image %(name)s exists, but contains different "
|
|
||||||
"checksums. Updating.",
|
|
||||||
{'name': name},
|
|
||||||
)
|
|
||||||
|
|
||||||
if disable_vendor_agent:
|
|
||||||
kwargs.update(
|
|
||||||
self._connection.config.config['disable_vendor_agent']
|
|
||||||
)
|
|
||||||
|
|
||||||
# If a user used the v1 calling format, they will have
|
|
||||||
# passed a dict called properties along
|
|
||||||
properties = kwargs.pop('properties', {})
|
|
||||||
properties[self._IMAGE_MD5_KEY] = md5 or ''
|
|
||||||
properties[self._IMAGE_SHA256_KEY] = sha256 or ''
|
|
||||||
properties[self._IMAGE_OBJECT_KEY] = '/'.join([container, name])
|
|
||||||
kwargs.update(properties)
|
|
||||||
image_kwargs = {'properties': kwargs}
|
|
||||||
if disk_format:
|
|
||||||
image_kwargs['disk_format'] = disk_format
|
|
||||||
if container_format:
|
|
||||||
image_kwargs['container_format'] = container_format
|
|
||||||
if tags:
|
|
||||||
image_kwargs['tags'] = tags
|
|
||||||
|
|
||||||
if filename or data:
|
|
||||||
image = self._upload_image(
|
|
||||||
name,
|
|
||||||
filename=filename,
|
|
||||||
data=data,
|
|
||||||
meta=meta,
|
|
||||||
wait=wait,
|
|
||||||
timeout=timeout,
|
|
||||||
validate_checksum=validate_checksum,
|
|
||||||
use_import=use_import,
|
|
||||||
stores=stores,
|
|
||||||
all_stores=stores,
|
|
||||||
all_stores_must_succeed=stores,
|
|
||||||
**image_kwargs,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
image_kwargs['name'] = name
|
|
||||||
image = self._create_image(**image_kwargs)
|
|
||||||
|
|
||||||
self._connection._get_cache(None).invalidate()
|
|
||||||
|
|
||||||
return image
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _create_image(self, name, **image_kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _upload_image(
|
|
||||||
self,
|
|
||||||
name,
|
|
||||||
filename,
|
|
||||||
data,
|
|
||||||
meta,
|
|
||||||
wait,
|
|
||||||
timeout,
|
|
||||||
validate_checksum=True,
|
|
||||||
use_import=False,
|
|
||||||
stores=None,
|
|
||||||
all_stores=None,
|
|
||||||
all_stores_must_succeed=None,
|
|
||||||
**image_kwargs,
|
|
||||||
):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _update_image_properties(self, image, meta, properties):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def update_image_properties(
|
|
||||||
self,
|
|
||||||
image=None,
|
|
||||||
meta=None,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Update the properties of an existing image.
|
|
||||||
|
|
||||||
:param image: Name or id of an image or an Image object.
|
|
||||||
:param meta: A dict of key/value pairs to use for metadata that
|
|
||||||
bypasses automatic type conversion.
|
|
||||||
|
|
||||||
Additional kwargs will be passed to the image creation as additional
|
|
||||||
metadata for the image and will have all values converted to string
|
|
||||||
except for min_disk, min_ram, size and virtual_size which will be
|
|
||||||
converted to int.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if isinstance(image, str):
|
|
||||||
image = self._connection.get_image(image)
|
|
||||||
|
|
||||||
if not meta:
|
|
||||||
meta = {}
|
|
||||||
|
|
||||||
img_props = {}
|
|
||||||
for k, v in iter(kwargs.items()):
|
|
||||||
if v and k in ['ramdisk', 'kernel']:
|
|
||||||
v = self._connection.get_image_id(v)
|
|
||||||
k = '{0}_id'.format(k)
|
|
||||||
img_props[k] = v
|
|
||||||
|
|
||||||
return self._update_image_properties(image, meta, img_props)
|
|
@@ -9,15 +9,261 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import os
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from openstack.cloud import exc
|
from openstack.cloud import exc
|
||||||
from openstack import exceptions
|
from openstack import exceptions
|
||||||
from openstack.image import _base_proxy
|
|
||||||
from openstack.image.v1 import image as _image
|
from openstack.image.v1 import image as _image
|
||||||
|
from openstack import proxy
|
||||||
|
from openstack import utils
|
||||||
|
|
||||||
|
|
||||||
class Proxy(_base_proxy.BaseImageProxy):
|
def _get_name_and_filename(name, image_format):
|
||||||
|
# See if name points to an existing file
|
||||||
|
if os.path.exists(name):
|
||||||
|
# Neat. Easy enough
|
||||||
|
return os.path.splitext(os.path.basename(name))[0], name
|
||||||
|
|
||||||
|
# Try appending the disk format
|
||||||
|
name_with_ext = '.'.join((name, image_format))
|
||||||
|
if os.path.exists(name_with_ext):
|
||||||
|
return os.path.basename(name), name_with_ext
|
||||||
|
|
||||||
|
return name, None
|
||||||
|
|
||||||
|
|
||||||
|
class Proxy(proxy.Proxy):
|
||||||
|
retriable_status_codes = [503]
|
||||||
|
|
||||||
|
_IMAGE_MD5_KEY = 'owner_specified.openstack.md5'
|
||||||
|
_IMAGE_SHA256_KEY = 'owner_specified.openstack.sha256'
|
||||||
|
_IMAGE_OBJECT_KEY = 'owner_specified.openstack.object'
|
||||||
|
|
||||||
|
# NOTE(shade) shade keys were owner_specified.shade.md5 - we need to add
|
||||||
|
# those to freshness checks so that a shade->sdk transition
|
||||||
|
# doesn't result in a re-upload
|
||||||
|
_SHADE_IMAGE_MD5_KEY = 'owner_specified.shade.md5'
|
||||||
|
_SHADE_IMAGE_SHA256_KEY = 'owner_specified.shade.sha256'
|
||||||
|
_SHADE_IMAGE_OBJECT_KEY = 'owner_specified.shade.object'
|
||||||
|
|
||||||
|
# ====== IMAGES ======
|
||||||
|
def create_image(
|
||||||
|
self,
|
||||||
|
name,
|
||||||
|
filename=None,
|
||||||
|
container=None,
|
||||||
|
md5=None,
|
||||||
|
sha256=None,
|
||||||
|
disk_format=None,
|
||||||
|
container_format=None,
|
||||||
|
disable_vendor_agent=True,
|
||||||
|
allow_duplicates=False,
|
||||||
|
meta=None,
|
||||||
|
wait=False,
|
||||||
|
timeout=3600,
|
||||||
|
data=None,
|
||||||
|
validate_checksum=False,
|
||||||
|
use_import=False,
|
||||||
|
stores=None,
|
||||||
|
tags=None,
|
||||||
|
all_stores=None,
|
||||||
|
all_stores_must_succeed=None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""Upload an image.
|
||||||
|
|
||||||
|
:param str name: Name of the image to create. If it is a pathname
|
||||||
|
of an image, the name will be constructed from the extensionless
|
||||||
|
basename of the path.
|
||||||
|
:param str filename: The path to the file to upload, if needed.
|
||||||
|
(optional, defaults to None)
|
||||||
|
:param data: Image data (string or file-like object). It is mutually
|
||||||
|
exclusive with filename
|
||||||
|
:param str container: Name of the container in swift where images
|
||||||
|
should be uploaded for import if the cloud requires such a thing.
|
||||||
|
(optional, defaults to 'images')
|
||||||
|
:param str md5: md5 sum of the image file. If not given, an md5 will
|
||||||
|
be calculated.
|
||||||
|
:param str sha256: sha256 sum of the image file. If not given, an md5
|
||||||
|
will be calculated.
|
||||||
|
:param str disk_format: The disk format the image is in. (optional,
|
||||||
|
defaults to the os-client-config config value for this cloud)
|
||||||
|
:param str container_format: The container format the image is in.
|
||||||
|
(optional, defaults to the os-client-config config value for this
|
||||||
|
cloud)
|
||||||
|
:param list tags: List of tags for this image. Each tag is a string
|
||||||
|
of at most 255 chars.
|
||||||
|
:param bool disable_vendor_agent: Whether or not to append metadata
|
||||||
|
flags to the image to inform the cloud in question to not expect a
|
||||||
|
vendor agent to be runing. (optional, defaults to True)
|
||||||
|
:param allow_duplicates: If true, skips checks that enforce unique
|
||||||
|
image name. (optional, defaults to False)
|
||||||
|
:param meta: A dict of key/value pairs to use for metadata that
|
||||||
|
bypasses automatic type conversion.
|
||||||
|
:param bool wait: If true, waits for image to be created. Defaults to
|
||||||
|
true - however, be aware that one of the upload methods is always
|
||||||
|
synchronous.
|
||||||
|
:param timeout: Seconds to wait for image creation. None is forever.
|
||||||
|
:param bool validate_checksum: If true and cloud returns checksum,
|
||||||
|
compares return value with the one calculated or passed into this
|
||||||
|
call. If value does not match - raises exception. Default is
|
||||||
|
'false'
|
||||||
|
:param bool use_import: Use the interoperable image import mechanism
|
||||||
|
to import the image. This defaults to false because it is harder on
|
||||||
|
the target cloud so should only be used when needed, such as when
|
||||||
|
the user needs the cloud to transform image format. If the cloud
|
||||||
|
has disabled direct uploads, this will default to true.
|
||||||
|
:param stores:
|
||||||
|
List of stores to be used when enabled_backends is activated
|
||||||
|
in glance. List values can be the id of a store or a
|
||||||
|
:class:`~openstack.image.v2.service_info.Store` instance.
|
||||||
|
Implies ``use_import`` equals ``True``.
|
||||||
|
:param all_stores:
|
||||||
|
Upload to all available stores. Mutually exclusive with
|
||||||
|
``store`` and ``stores``.
|
||||||
|
Implies ``use_import`` equals ``True``.
|
||||||
|
:param all_stores_must_succeed:
|
||||||
|
When set to True, if an error occurs during the upload in at
|
||||||
|
least one store, the worfklow fails, the data is deleted
|
||||||
|
from stores where copying is done (not staging), and the
|
||||||
|
state of the image is unchanged. When set to False, the
|
||||||
|
workflow will fail (data deleted from stores, …) only if the
|
||||||
|
import fails on all stores specified by the user. In case of
|
||||||
|
a partial success, the locations added to the image will be
|
||||||
|
the stores where the data has been correctly uploaded.
|
||||||
|
Default is True.
|
||||||
|
Implies ``use_import`` equals ``True``.
|
||||||
|
|
||||||
|
Additional kwargs will be passed to the image creation as additional
|
||||||
|
metadata for the image and will have all values converted to string
|
||||||
|
except for min_disk, min_ram, size and virtual_size which will be
|
||||||
|
converted to int.
|
||||||
|
|
||||||
|
If you are sure you have all of your data types correct or have an
|
||||||
|
advanced need to be explicit, use meta. If you are just a normal
|
||||||
|
consumer, using kwargs is likely the right choice.
|
||||||
|
|
||||||
|
If a value is in meta and kwargs, meta wins.
|
||||||
|
|
||||||
|
:returns: A ``munch.Munch`` of the Image object
|
||||||
|
:raises: SDKException if there are problems uploading
|
||||||
|
"""
|
||||||
|
if container is None:
|
||||||
|
container = self._connection._OBJECT_AUTOCREATE_CONTAINER
|
||||||
|
|
||||||
|
if not meta:
|
||||||
|
meta = {}
|
||||||
|
|
||||||
|
if not disk_format:
|
||||||
|
disk_format = self._connection.config.config['image_format']
|
||||||
|
|
||||||
|
if not container_format:
|
||||||
|
# https://docs.openstack.org/image-guide/image-formats.html
|
||||||
|
container_format = 'bare'
|
||||||
|
|
||||||
|
if data and filename:
|
||||||
|
raise exceptions.SDKException(
|
||||||
|
'Passing filename and data simultaneously is not supported'
|
||||||
|
)
|
||||||
|
|
||||||
|
# If there is no filename, see if name is actually the filename
|
||||||
|
if not filename and not data:
|
||||||
|
name, filename = _get_name_and_filename(
|
||||||
|
name,
|
||||||
|
self._connection.config.config['image_format'],
|
||||||
|
)
|
||||||
|
|
||||||
|
if validate_checksum and data and not isinstance(data, bytes):
|
||||||
|
raise exceptions.SDKException(
|
||||||
|
'Validating checksum is not possible when data is not a '
|
||||||
|
'direct binary object'
|
||||||
|
)
|
||||||
|
|
||||||
|
if not (md5 or sha256) and validate_checksum:
|
||||||
|
if filename:
|
||||||
|
md5, sha256 = utils._get_file_hashes(filename)
|
||||||
|
elif data and isinstance(data, bytes):
|
||||||
|
md5, sha256 = utils._calculate_data_hashes(data)
|
||||||
|
|
||||||
|
if allow_duplicates:
|
||||||
|
current_image = None
|
||||||
|
else:
|
||||||
|
current_image = self.find_image(name)
|
||||||
|
if current_image:
|
||||||
|
# NOTE(pas-ha) 'properties' may be absent or be None
|
||||||
|
props = current_image.get('properties') or {}
|
||||||
|
md5_key = props.get(
|
||||||
|
self._IMAGE_MD5_KEY,
|
||||||
|
props.get(self._SHADE_IMAGE_MD5_KEY, ''),
|
||||||
|
)
|
||||||
|
sha256_key = props.get(
|
||||||
|
self._IMAGE_SHA256_KEY,
|
||||||
|
props.get(self._SHADE_IMAGE_SHA256_KEY, ''),
|
||||||
|
)
|
||||||
|
up_to_date = utils._hashes_up_to_date(
|
||||||
|
md5=md5,
|
||||||
|
sha256=sha256,
|
||||||
|
md5_key=md5_key,
|
||||||
|
sha256_key=sha256_key,
|
||||||
|
)
|
||||||
|
if up_to_date:
|
||||||
|
self.log.debug(
|
||||||
|
"image %(name)s exists and is up to date",
|
||||||
|
{'name': name},
|
||||||
|
)
|
||||||
|
return current_image
|
||||||
|
else:
|
||||||
|
self.log.debug(
|
||||||
|
"image %(name)s exists, but contains different "
|
||||||
|
"checksums. Updating.",
|
||||||
|
{'name': name},
|
||||||
|
)
|
||||||
|
|
||||||
|
if disable_vendor_agent:
|
||||||
|
kwargs.update(
|
||||||
|
self._connection.config.config['disable_vendor_agent']
|
||||||
|
)
|
||||||
|
|
||||||
|
# If a user used the v1 calling format, they will have
|
||||||
|
# passed a dict called properties along
|
||||||
|
properties = kwargs.pop('properties', {})
|
||||||
|
properties[self._IMAGE_MD5_KEY] = md5 or ''
|
||||||
|
properties[self._IMAGE_SHA256_KEY] = sha256 or ''
|
||||||
|
properties[self._IMAGE_OBJECT_KEY] = '/'.join([container, name])
|
||||||
|
kwargs.update(properties)
|
||||||
|
image_kwargs = {'properties': kwargs}
|
||||||
|
if disk_format:
|
||||||
|
image_kwargs['disk_format'] = disk_format
|
||||||
|
if container_format:
|
||||||
|
image_kwargs['container_format'] = container_format
|
||||||
|
if tags:
|
||||||
|
image_kwargs['tags'] = tags
|
||||||
|
|
||||||
|
if filename or data:
|
||||||
|
image = self._upload_image(
|
||||||
|
name,
|
||||||
|
filename=filename,
|
||||||
|
data=data,
|
||||||
|
meta=meta,
|
||||||
|
wait=wait,
|
||||||
|
timeout=timeout,
|
||||||
|
validate_checksum=validate_checksum,
|
||||||
|
use_import=use_import,
|
||||||
|
stores=stores,
|
||||||
|
all_stores=stores,
|
||||||
|
all_stores_must_succeed=stores,
|
||||||
|
**image_kwargs,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
image_kwargs['name'] = name
|
||||||
|
image = self._create_image(**image_kwargs)
|
||||||
|
|
||||||
|
self._connection._get_cache(None).invalidate()
|
||||||
|
|
||||||
|
return image
|
||||||
|
|
||||||
def _create_image(self, **kwargs):
|
def _create_image(self, **kwargs):
|
||||||
"""Create image resource from attributes"""
|
"""Create image resource from attributes"""
|
||||||
return self._create(_image.Image, **kwargs)
|
return self._create(_image.Image, **kwargs)
|
||||||
@@ -107,18 +353,6 @@ class Proxy(_base_proxy.BaseImageProxy):
|
|||||||
raise
|
raise
|
||||||
return image
|
return image
|
||||||
|
|
||||||
def _update_image_properties(self, image, meta, properties):
|
|
||||||
properties.update(meta)
|
|
||||||
img_props = {}
|
|
||||||
for k, v in iter(properties.items()):
|
|
||||||
if image.properties.get(k, None) != v:
|
|
||||||
img_props['x-image-meta-{key}'.format(key=k)] = v
|
|
||||||
if not img_props:
|
|
||||||
return False
|
|
||||||
self.put('/images/{id}'.format(id=image.id), headers=img_props)
|
|
||||||
self._connection.list_images.invalidate(self._connection)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _existing_image(self, **kwargs):
|
def _existing_image(self, **kwargs):
|
||||||
return _image.Image.existing(connection=self._connection, **kwargs)
|
return _image.Image.existing(connection=self._connection, **kwargs)
|
||||||
|
|
||||||
@@ -234,3 +468,49 @@ class Proxy(_base_proxy.BaseImageProxy):
|
|||||||
output=output,
|
output=output,
|
||||||
chunk_size=chunk_size,
|
chunk_size=chunk_size,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _update_image_properties(self, image, meta, properties):
|
||||||
|
properties.update(meta)
|
||||||
|
img_props = {}
|
||||||
|
for k, v in iter(properties.items()):
|
||||||
|
if image.properties.get(k, None) != v:
|
||||||
|
img_props['x-image-meta-{key}'.format(key=k)] = v
|
||||||
|
if not img_props:
|
||||||
|
return False
|
||||||
|
self.put('/images/{id}'.format(id=image.id), headers=img_props)
|
||||||
|
self._connection.list_images.invalidate(self._connection)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def update_image_properties(
|
||||||
|
self,
|
||||||
|
image=None,
|
||||||
|
meta=None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Update the properties of an existing image.
|
||||||
|
|
||||||
|
:param image: Name or id of an image or an Image object.
|
||||||
|
:param meta: A dict of key/value pairs to use for metadata that
|
||||||
|
bypasses automatic type conversion.
|
||||||
|
|
||||||
|
Additional kwargs will be passed to the image creation as additional
|
||||||
|
metadata for the image and will have all values converted to string
|
||||||
|
except for min_disk, min_ram, size and virtual_size which will be
|
||||||
|
converted to int.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(image, str):
|
||||||
|
image = self._connection.get_image(image)
|
||||||
|
|
||||||
|
if not meta:
|
||||||
|
meta = {}
|
||||||
|
|
||||||
|
img_props = {}
|
||||||
|
for k, v in iter(kwargs.items()):
|
||||||
|
if v and k in ['ramdisk', 'kernel']:
|
||||||
|
v = self._connection.get_image_id(v)
|
||||||
|
k = '{0}_id'.format(k)
|
||||||
|
img_props[k] = v
|
||||||
|
|
||||||
|
return self._update_image_properties(image, meta, img_props)
|
||||||
|
@@ -10,11 +10,11 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from openstack import exceptions
|
from openstack import exceptions
|
||||||
from openstack.image import _base_proxy
|
|
||||||
from openstack.image.v2 import image as _image
|
from openstack.image.v2 import image as _image
|
||||||
from openstack.image.v2 import member as _member
|
from openstack.image.v2 import member as _member
|
||||||
from openstack.image.v2 import metadef_namespace as _metadef_namespace
|
from openstack.image.v2 import metadef_namespace as _metadef_namespace
|
||||||
@@ -23,6 +23,7 @@ from openstack.image.v2 import metadef_schema as _metadef_schema
|
|||||||
from openstack.image.v2 import schema as _schema
|
from openstack.image.v2 import schema as _schema
|
||||||
from openstack.image.v2 import service_info as _si
|
from openstack.image.v2 import service_info as _si
|
||||||
from openstack.image.v2 import task as _task
|
from openstack.image.v2 import task as _task
|
||||||
|
from openstack import proxy
|
||||||
from openstack import resource
|
from openstack import resource
|
||||||
from openstack import utils
|
from openstack import utils
|
||||||
|
|
||||||
@@ -32,7 +33,22 @@ _INT_PROPERTIES = ('min_disk', 'min_ram', 'size', 'virtual_size')
|
|||||||
_RAW_PROPERTIES = ('is_protected', 'protected', 'tags')
|
_RAW_PROPERTIES = ('is_protected', 'protected', 'tags')
|
||||||
|
|
||||||
|
|
||||||
class Proxy(_base_proxy.BaseImageProxy):
|
def _get_name_and_filename(name, image_format):
|
||||||
|
# See if name points to an existing file
|
||||||
|
if os.path.exists(name):
|
||||||
|
# Neat. Easy enough
|
||||||
|
return os.path.splitext(os.path.basename(name))[0], name
|
||||||
|
|
||||||
|
# Try appending the disk format
|
||||||
|
name_with_ext = '.'.join((name, image_format))
|
||||||
|
if os.path.exists(name_with_ext):
|
||||||
|
return os.path.basename(name), name_with_ext
|
||||||
|
|
||||||
|
return name, None
|
||||||
|
|
||||||
|
|
||||||
|
class Proxy(proxy.Proxy):
|
||||||
|
|
||||||
_resource_registry = {
|
_resource_registry = {
|
||||||
"image": _image.Image,
|
"image": _image.Image,
|
||||||
"image_member": _member.Member,
|
"image_member": _member.Member,
|
||||||
@@ -43,7 +59,232 @@ class Proxy(_base_proxy.BaseImageProxy):
|
|||||||
"task": _task.Task,
|
"task": _task.Task,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
retriable_status_codes = [503]
|
||||||
|
|
||||||
|
_IMAGE_MD5_KEY = 'owner_specified.openstack.md5'
|
||||||
|
_IMAGE_SHA256_KEY = 'owner_specified.openstack.sha256'
|
||||||
|
_IMAGE_OBJECT_KEY = 'owner_specified.openstack.object'
|
||||||
|
|
||||||
|
# NOTE(shade) shade keys were owner_specified.shade.md5 - we need to add
|
||||||
|
# those to freshness checks so that a shade->sdk transition
|
||||||
|
# doesn't result in a re-upload
|
||||||
|
_SHADE_IMAGE_MD5_KEY = 'owner_specified.shade.md5'
|
||||||
|
_SHADE_IMAGE_SHA256_KEY = 'owner_specified.shade.sha256'
|
||||||
|
_SHADE_IMAGE_OBJECT_KEY = 'owner_specified.shade.object'
|
||||||
|
|
||||||
# ====== IMAGES ======
|
# ====== IMAGES ======
|
||||||
|
def create_image(
|
||||||
|
self,
|
||||||
|
name,
|
||||||
|
filename=None,
|
||||||
|
container=None,
|
||||||
|
md5=None,
|
||||||
|
sha256=None,
|
||||||
|
disk_format=None,
|
||||||
|
container_format=None,
|
||||||
|
disable_vendor_agent=True,
|
||||||
|
allow_duplicates=False,
|
||||||
|
meta=None,
|
||||||
|
wait=False,
|
||||||
|
timeout=3600,
|
||||||
|
data=None,
|
||||||
|
validate_checksum=False,
|
||||||
|
use_import=False,
|
||||||
|
stores=None,
|
||||||
|
tags=None,
|
||||||
|
all_stores=None,
|
||||||
|
all_stores_must_succeed=None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""Upload an image.
|
||||||
|
|
||||||
|
:param str name: Name of the image to create. If it is a pathname
|
||||||
|
of an image, the name will be constructed from the extensionless
|
||||||
|
basename of the path.
|
||||||
|
:param str filename: The path to the file to upload, if needed.
|
||||||
|
(optional, defaults to None)
|
||||||
|
:param data: Image data (string or file-like object). It is mutually
|
||||||
|
exclusive with filename
|
||||||
|
:param str container: Name of the container in swift where images
|
||||||
|
should be uploaded for import if the cloud requires such a thing.
|
||||||
|
(optional, defaults to 'images')
|
||||||
|
:param str md5: md5 sum of the image file. If not given, an md5 will
|
||||||
|
be calculated.
|
||||||
|
:param str sha256: sha256 sum of the image file. If not given, an md5
|
||||||
|
will be calculated.
|
||||||
|
:param str disk_format: The disk format the image is in. (optional,
|
||||||
|
defaults to the os-client-config config value for this cloud)
|
||||||
|
:param str container_format: The container format the image is in.
|
||||||
|
(optional, defaults to the os-client-config config value for this
|
||||||
|
cloud)
|
||||||
|
:param list tags: List of tags for this image. Each tag is a string
|
||||||
|
of at most 255 chars.
|
||||||
|
:param bool disable_vendor_agent: Whether or not to append metadata
|
||||||
|
flags to the image to inform the cloud in question to not expect a
|
||||||
|
vendor agent to be runing. (optional, defaults to True)
|
||||||
|
:param allow_duplicates: If true, skips checks that enforce unique
|
||||||
|
image name. (optional, defaults to False)
|
||||||
|
:param meta: A dict of key/value pairs to use for metadata that
|
||||||
|
bypasses automatic type conversion.
|
||||||
|
:param bool wait: If true, waits for image to be created. Defaults to
|
||||||
|
true - however, be aware that one of the upload methods is always
|
||||||
|
synchronous.
|
||||||
|
:param timeout: Seconds to wait for image creation. None is forever.
|
||||||
|
:param bool validate_checksum: If true and cloud returns checksum,
|
||||||
|
compares return value with the one calculated or passed into this
|
||||||
|
call. If value does not match - raises exception. Default is
|
||||||
|
'false'
|
||||||
|
:param bool use_import: Use the interoperable image import mechanism
|
||||||
|
to import the image. This defaults to false because it is harder on
|
||||||
|
the target cloud so should only be used when needed, such as when
|
||||||
|
the user needs the cloud to transform image format. If the cloud
|
||||||
|
has disabled direct uploads, this will default to true.
|
||||||
|
:param stores: List of stores to be used when enabled_backends is
|
||||||
|
activated in glance. List values can be the id of a store or a
|
||||||
|
:class:`~openstack.image.v2.service_info.Store` instance.
|
||||||
|
Implies ``use_import`` equals ``True``.
|
||||||
|
:param all_stores: Upload to all available stores. Mutually exclusive
|
||||||
|
with ``store`` and ``stores``.
|
||||||
|
Implies ``use_import`` equals ``True``.
|
||||||
|
:param all_stores_must_succeed: When set to True, if an error occurs
|
||||||
|
during the upload in at least one store, the worfklow fails, the
|
||||||
|
data is deleted from stores where copying is done (not staging),
|
||||||
|
and the state of the image is unchanged. When set to False, the
|
||||||
|
workflow will fail (data deleted from stores, …) only if the import
|
||||||
|
fails on all stores specified by the user. In case of a partial
|
||||||
|
success, the locations added to the image will be the stores where
|
||||||
|
the data has been correctly uploaded.
|
||||||
|
Default is True.
|
||||||
|
Implies ``use_import`` equals ``True``.
|
||||||
|
|
||||||
|
Additional kwargs will be passed to the image creation as additional
|
||||||
|
metadata for the image and will have all values converted to string
|
||||||
|
except for min_disk, min_ram, size and virtual_size which will be
|
||||||
|
converted to int.
|
||||||
|
|
||||||
|
If you are sure you have all of your data types correct or have an
|
||||||
|
advanced need to be explicit, use meta. If you are just a normal
|
||||||
|
consumer, using kwargs is likely the right choice.
|
||||||
|
|
||||||
|
If a value is in meta and kwargs, meta wins.
|
||||||
|
|
||||||
|
:returns: A ``munch.Munch`` of the Image object
|
||||||
|
:raises: SDKException if there are problems uploading
|
||||||
|
"""
|
||||||
|
if container is None:
|
||||||
|
container = self._connection._OBJECT_AUTOCREATE_CONTAINER
|
||||||
|
|
||||||
|
if not meta:
|
||||||
|
meta = {}
|
||||||
|
|
||||||
|
if not disk_format:
|
||||||
|
disk_format = self._connection.config.config['image_format']
|
||||||
|
|
||||||
|
if not container_format:
|
||||||
|
# https://docs.openstack.org/image-guide/image-formats.html
|
||||||
|
container_format = 'bare'
|
||||||
|
|
||||||
|
if data and filename:
|
||||||
|
raise exceptions.SDKException(
|
||||||
|
'Passing filename and data simultaneously is not supported'
|
||||||
|
)
|
||||||
|
|
||||||
|
# If there is no filename, see if name is actually the filename
|
||||||
|
if not filename and not data:
|
||||||
|
name, filename = _get_name_and_filename(
|
||||||
|
name,
|
||||||
|
self._connection.config.config['image_format'],
|
||||||
|
)
|
||||||
|
|
||||||
|
if validate_checksum and data and not isinstance(data, bytes):
|
||||||
|
raise exceptions.SDKException(
|
||||||
|
'Validating checksum is not possible when data is not a '
|
||||||
|
'direct binary object'
|
||||||
|
)
|
||||||
|
|
||||||
|
if not (md5 or sha256) and validate_checksum:
|
||||||
|
if filename:
|
||||||
|
md5, sha256 = utils._get_file_hashes(filename)
|
||||||
|
elif data and isinstance(data, bytes):
|
||||||
|
md5, sha256 = utils._calculate_data_hashes(data)
|
||||||
|
|
||||||
|
if allow_duplicates:
|
||||||
|
current_image = None
|
||||||
|
else:
|
||||||
|
current_image = self.find_image(name)
|
||||||
|
if current_image:
|
||||||
|
# NOTE(pas-ha) 'properties' may be absent or be None
|
||||||
|
props = current_image.get('properties') or {}
|
||||||
|
md5_key = props.get(
|
||||||
|
self._IMAGE_MD5_KEY,
|
||||||
|
props.get(self._SHADE_IMAGE_MD5_KEY, ''),
|
||||||
|
)
|
||||||
|
sha256_key = props.get(
|
||||||
|
self._IMAGE_SHA256_KEY,
|
||||||
|
props.get(self._SHADE_IMAGE_SHA256_KEY, ''),
|
||||||
|
)
|
||||||
|
up_to_date = utils._hashes_up_to_date(
|
||||||
|
md5=md5,
|
||||||
|
sha256=sha256,
|
||||||
|
md5_key=md5_key,
|
||||||
|
sha256_key=sha256_key,
|
||||||
|
)
|
||||||
|
if up_to_date:
|
||||||
|
self.log.debug(
|
||||||
|
"image %(name)s exists and is up to date",
|
||||||
|
{'name': name},
|
||||||
|
)
|
||||||
|
return current_image
|
||||||
|
else:
|
||||||
|
self.log.debug(
|
||||||
|
"image %(name)s exists, but contains different "
|
||||||
|
"checksums. Updating.",
|
||||||
|
{'name': name},
|
||||||
|
)
|
||||||
|
|
||||||
|
if disable_vendor_agent:
|
||||||
|
kwargs.update(
|
||||||
|
self._connection.config.config['disable_vendor_agent']
|
||||||
|
)
|
||||||
|
|
||||||
|
# If a user used the v1 calling format, they will have
|
||||||
|
# passed a dict called properties along
|
||||||
|
properties = kwargs.pop('properties', {})
|
||||||
|
properties[self._IMAGE_MD5_KEY] = md5 or ''
|
||||||
|
properties[self._IMAGE_SHA256_KEY] = sha256 or ''
|
||||||
|
properties[self._IMAGE_OBJECT_KEY] = '/'.join([container, name])
|
||||||
|
kwargs.update(properties)
|
||||||
|
image_kwargs = {'properties': kwargs}
|
||||||
|
if disk_format:
|
||||||
|
image_kwargs['disk_format'] = disk_format
|
||||||
|
if container_format:
|
||||||
|
image_kwargs['container_format'] = container_format
|
||||||
|
if tags:
|
||||||
|
image_kwargs['tags'] = tags
|
||||||
|
|
||||||
|
if filename or data:
|
||||||
|
image = self._upload_image(
|
||||||
|
name,
|
||||||
|
filename=filename,
|
||||||
|
data=data,
|
||||||
|
meta=meta,
|
||||||
|
wait=wait,
|
||||||
|
timeout=timeout,
|
||||||
|
validate_checksum=validate_checksum,
|
||||||
|
use_import=use_import,
|
||||||
|
stores=stores,
|
||||||
|
all_stores=stores,
|
||||||
|
all_stores_must_succeed=stores,
|
||||||
|
**image_kwargs,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
image_kwargs['name'] = name
|
||||||
|
image = self._create_image(**image_kwargs)
|
||||||
|
|
||||||
|
self._connection._get_cache(None).invalidate()
|
||||||
|
|
||||||
|
return image
|
||||||
|
|
||||||
def _create_image(self, **kwargs):
|
def _create_image(self, **kwargs):
|
||||||
"""Create image resource from attributes"""
|
"""Create image resource from attributes"""
|
||||||
return self._create(_image.Image, **kwargs)
|
return self._create(_image.Image, **kwargs)
|
||||||
@@ -462,25 +703,6 @@ class Proxy(_base_proxy.BaseImageProxy):
|
|||||||
else:
|
else:
|
||||||
return glance_task
|
return glance_task
|
||||||
|
|
||||||
def _update_image_properties(self, image, meta, properties):
|
|
||||||
if not isinstance(image, _image.Image):
|
|
||||||
# If we come here with a dict (cloud) - convert dict to real object
|
|
||||||
# to properly consume all properties (to calculate the diff).
|
|
||||||
# This currently happens from unittests.
|
|
||||||
image = _image.Image.existing(**image)
|
|
||||||
img_props = image.properties.copy()
|
|
||||||
|
|
||||||
for k, v in iter(self._make_v2_image_params(meta, properties).items()):
|
|
||||||
if image.get(k, None) != v:
|
|
||||||
img_props[k] = v
|
|
||||||
if not img_props:
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.update_image(image, **img_props)
|
|
||||||
|
|
||||||
self._connection.list_images.invalidate(self._connection)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _existing_image(self, **kwargs):
|
def _existing_image(self, **kwargs):
|
||||||
return _image.Image.existing(connection=self._connection, **kwargs)
|
return _image.Image.existing(connection=self._connection, **kwargs)
|
||||||
|
|
||||||
@@ -624,6 +846,59 @@ class Proxy(_base_proxy.BaseImageProxy):
|
|||||||
image = self._get_resource(_image.Image, image)
|
image = self._get_resource(_image.Image, image)
|
||||||
image.reactivate(self)
|
image.reactivate(self)
|
||||||
|
|
||||||
|
def _update_image_properties(self, image, meta, properties):
|
||||||
|
if not isinstance(image, _image.Image):
|
||||||
|
# If we come here with a dict (cloud) - convert dict to real object
|
||||||
|
# to properly consume all properties (to calculate the diff).
|
||||||
|
# This currently happens from unittests.
|
||||||
|
image = _image.Image.existing(**image)
|
||||||
|
img_props = image.properties.copy()
|
||||||
|
|
||||||
|
for k, v in iter(self._make_v2_image_params(meta, properties).items()):
|
||||||
|
if image.get(k, None) != v:
|
||||||
|
img_props[k] = v
|
||||||
|
if not img_props:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.update_image(image, **img_props)
|
||||||
|
|
||||||
|
self._connection.list_images.invalidate(self._connection)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def update_image_properties(
|
||||||
|
self,
|
||||||
|
image=None,
|
||||||
|
meta=None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Update the properties of an existing image.
|
||||||
|
|
||||||
|
:param image: Name or id of an image or an Image object.
|
||||||
|
:param meta: A dict of key/value pairs to use for metadata that
|
||||||
|
bypasses automatic type conversion.
|
||||||
|
|
||||||
|
Additional kwargs will be passed to the image creation as additional
|
||||||
|
metadata for the image and will have all values converted to string
|
||||||
|
except for min_disk, min_ram, size and virtual_size which will be
|
||||||
|
converted to int.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(image, str):
|
||||||
|
image = self._connection.get_image(image)
|
||||||
|
|
||||||
|
if not meta:
|
||||||
|
meta = {}
|
||||||
|
|
||||||
|
img_props = {}
|
||||||
|
for k, v in iter(kwargs.items()):
|
||||||
|
if v and k in ['ramdisk', 'kernel']:
|
||||||
|
v = self._connection.get_image_id(v)
|
||||||
|
k = '{0}_id'.format(k)
|
||||||
|
img_props[k] = v
|
||||||
|
|
||||||
|
return self._update_image_properties(image, meta, img_props)
|
||||||
|
|
||||||
def add_tag(self, image, tag):
|
def add_tag(self, image, tag):
|
||||||
"""Add a tag to an image
|
"""Add a tag to an image
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user