Repository: ansible/ansible
Pull request: Add azure image module support create and delete
##### SUMMARY
<!--- Describe the change, including rationale and design decisions -->
Add Azure Image module support
It supports
- Create an image from an existing VM.
- Create an image from OS disk and data disk with OS type specific, where the disk can be a disk, blob uri or snapshot.
- Delete an existing image.

<!---
If you are fixing an existing issue, please include "Fixes #nnn" in your
commit message and your description; but you should still explain what
the change does.
-->
 Fix #30940

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - New Module Pull Request

##### COMPONENT NAME
<!--- Name of the module/plugin/module/task -->
azure_rm_image.py

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
2.5
```


##### ADDITIONAL INFORMATION
<!---
Include additional information to help people understand the change here.
For bugs that don't have a linked bug report, a step-by-step reproduction
of the problem is helpful.
  -->

<!--- Paste verbatim command output below, e.g. before and after your change -->
```

```


Problem statement:
Azure module for Managed Images
It would be greate if a module for Azure could be writting to interact with Managed Images. Currently, Azure supports two methonds for interacting with images. 1. Images from the Marketplace that someone else builds and maintains. 2. Managed Images that I'm responsible for building and maintaining, and appling custom packages.

Could a module be written to interact with Managed Images? The CLI arguments start with "az image", and there's concepts of list/create/delete. 

This would potentially let us manage these images with tower.

##### ISSUE TYPE
 - Feature Idea

##### COMPONENT NAME
azurerm modules

Hints from issue discussion:
cc @dharmabumstead @jwhitbeck
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
Hi @codyja, there are currently 2 PR's available that will add these features. If you have any time it would be good if you could test them out and give your feedback on each page.

A PR for modules to create images and generate facts from images https://github.com/ansible/ansible/pull/32403

A PR to allow azure_rm_virtualmachine to create a host from a custom image https://github.com/ansible/ansible/pull/32367

Commit message: update for python3

Patch to review:
diff --git a/lib/ansible/module_utils/azure_rm_common.py b/lib/ansible/module_utils/azure_rm_common.py
index f40ddfcc8eaa89..3a4607b24dbd38 100644
--- a/lib/ansible/module_utils/azure_rm_common.py
+++ b/lib/ansible/module_utils/azure_rm_common.py
@@ -110,6 +110,7 @@
 try:
     from enum import Enum
     from msrestazure.azure_exceptions import CloudError
+    from msrestazure.tools import resource_id, is_valid_resource_id
     from msrestazure import azure_cloud
     from azure.mgmt.network.models import PublicIPAddress, NetworkSecurityGroup, SecurityRule, NetworkInterface, \
         NetworkInterfaceIPConfiguration, Subnet
@@ -151,6 +152,13 @@ def azure_id_to_dict(id):
     return result
 
 
+def format_resource_id(val, subscription_id, namespace, types, resource_group):
+    return resource_id(name=val,
+                       resource_group=resource_group,
+                       namespace=namespace,
+                       type=types,
+                       subscription=subscription_id) if not is_valid_resource_id(val) else val
+
 AZURE_PKG_VERSIONS = {
     StorageManagementClient.__name__: {
         'package_name': 'storage',
diff --git a/lib/ansible/modules/cloud/azure/azure_rm_image.py b/lib/ansible/modules/cloud/azure/azure_rm_image.py
new file mode 100644
index 00000000000000..143d3d767c242f
--- /dev/null
+++ b/lib/ansible/modules/cloud/azure/azure_rm_image.py
@@ -0,0 +1,372 @@
+#!/usr/bin/python
+#
+# Copyright (c) 2017 Yuwei Zhou, <yuwzho@microsoft.com>
+#
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+                    'status': ['preview'],
+                    'supported_by': 'certified'}
+
+
+DOCUMENTATION = '''
+---
+module: azure_rm_image
+version_added: "2.5"
+short_description: Manage Azure image.
+description:
+    - Create, delete an image from virtual machine, blob uri, managed disk or snapshot.
+options:
+    resource_group:
+        description:
+            - Name of resource group.
+        required: true
+    name:
+        description:
+            - Name of the image.
+        required: true
+    source:
+        description:
+            - OS disk source from the same region, including a virtual machine id or name,
+              OS disk blob uri, managed OS disk id or name, or OS snapshot id or name.
+        required: true
+    data_disk_sources:
+        description:
+            - List of data disk sources, including unmanaged blob uri, managed disk id or name, or snapshot id or name.
+        required: false
+    location:
+        description:
+            - Location of the image.
+        required: false
+    os_type:
+        description: The OS type of image.
+        choices:
+            - Windows
+            - Linux
+    state:
+        description:
+            - Assert the state of the image. Use 'present' to create or update a image and 'absent' to delete an image.
+        required: false
+        default: present
+        choices:
+            - absent
+            - present
+
+extends_documentation_fragment:
+    - azure
+    - azure_tags
+
+author:
+    - "Yuwei Zhou (@yuwzho)"
+
+'''
+
+EXAMPLES = '''
+- name: Create an image from a virtual machine
+  azure_rm_image:
+    resource_group: Testing
+    name: foobar
+    source: testvm001
+
+- name: Create an image from os disk
+  azure_rm_image:
+    resource_group: Testing
+    name: foobar
+    source: /subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Compute/disks/disk001
+    data_disk_sources:
+        - datadisk001
+        - datadisk002
+    os_type: Linux
+
+- name: Delete an image
+  azure_rm_image:
+    state: absent
+    resource_group: Testing
+    name: foobar
+    source: testvm001
+'''
+
+RETURN = '''
+state:
+    description: Current state of the image.
+    returned: success
+    type: complex
+    contains:
+        id:
+          description: Image resource path.
+          type: str
+          example: "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Compute/images/foobar"
+        name:
+          description: Image name.
+          type: str
+          example: "foobar"
+        location:
+          description: Location of the image
+          type: str
+          example: "eastus"
+        resource_group:
+          description: Resource group of the image
+          type: str
+          example: "Testing"
+        provisioning_state:
+          description: Success or failure of the provisioning event.
+          type: str
+          example: "Succeeded"
+        properties:
+          description: Facts about the current image
+          type: complex
+          contains: {
+              "storageProfile": {
+                "osDisk": {
+                "storageAccountType": "Standard_LRS",
+                "managedDisk": {
+                    "id": "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Compute/disks/testvm001"
+                },
+                "diskSizeGB": 32,
+                "osState": "Generalized",
+                "caching": "ReadOnly",
+                "osType": "Linux"
+                },
+                "dataDisks": [
+                    {
+                        "caching": "ReadOnly",
+                        "diskSizeGB": 64,
+                        "storageAccountType": "Standard_LRS",
+                        "managedDisk": {
+                        "id": "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Compute/disks/testvm001-datadisk-0"
+                        },
+                        "lun": 0
+                    }
+                ]
+            },
+            "sourceVirtualMachine": {
+                "id": "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Compute/virtualMachines/testvm001"
+            }
+          }
+'''  # NOQA
+
+from ansible.module_utils.azure_rm_common import AzureRMModuleBase, format_resource_id
+
+try:
+    from msrestazure.tools import parse_resource_id
+    from msrestazure.azure_exceptions import CloudError
+    from azure.mgmt.compute.models import SubResource, OperatingSystemStateTypes, ImageStorageProfile, Image, ImageOSDisk, ImageDataDisk
+except ImportError:
+    # This is handled in azure_rm_common
+    pass
+
+
+class AzureRMImage(AzureRMModuleBase):
+
+    def __init__(self):
+
+        self.module_arg_spec = dict(
+            resource_group=dict(type='str', required=True),
+            name=dict(type='str', required=True),
+            state=dict(type='str', default='present', choices=['present', 'absent']),
+            location=dict(type='str'),
+            source=dict(type='str', required=True),
+            data_disk_sources=dict(type='list', default=[]),
+            os_type=dict(type='str', choices=['Windows', 'Linux'])
+        )
+
+        self.results = dict(
+            changed=False,
+            state=dict()
+        )
+
+        self.resource_group = None
+        self.name = None
+        self.state = None
+        self.location = None
+        self.source = None
+        self.data_disk_sources = None
+        self.os_type = None
+        self.tags = None
+
+        super(AzureRMImage, self).__init__(self.module_arg_spec, supports_check_mode=True)
+
+    def exec_module(self, **kwargs):
+
+        for key in self.module_arg_spec:
+            setattr(self, key, kwargs[key])
+
+        results = dict()
+        changed = False
+        image = None
+
+        if not self.location:
+            # Set default location
+            resource_group = self.get_resource_group(self.resource_group)
+            self.location = resource_group.location
+
+        self.log('Fetching image {0}'.format(self.name))
+        image = self.get_image()
+        if image:
+            self.check_provisioning_state(image, self.state)
+            results = self._image_to_dict(image)
+            # update is not supported
+            if self.state == 'absent':
+                changed = True
+        # the image does not exist and create a new one
+        elif self.state == 'present':
+            changed = True
+
+        self.results['changed'] = changed
+        self.results['state'] = results
+
+        if not self.check_mode and changed:
+
+            if self.state == 'present':
+                # create from virtual machine
+                vm = self.get_source_vm()
+                if vm:
+                    if self.data_disk_sources:
+                        self.fail('data_disk_sources is not allowed when capturing image from vm')
+                    image = Image(self.location, source_virtual_machine=SubResource(vm.id))
+                else:
+                    if not self.os_type:
+                        self.fail('os_type is required to create the image')
+                    os_disk = self.create_os_disk()
+                    data_disks = self.create_data_disks()
+                    storage_profile = ImageStorageProfile(os_disk=os_disk, data_disks=data_disks)
+                    image = Image(self.location, storage_profile=storage_profile, tags=self.tags)
+                new_image = self.create_image(image)
+                self.results['state'] = self._image_to_dict(new_image)
+
+            elif self.state == 'absent':
+                # delete image
+                self.delete_image()
+                # the delete does not actually return anything. if no exception, then we'll assume
+                # it worked.
+                self.results['state']['status'] = 'Deleted'
+
+        return self.results
+
+    def _image_to_dict(self, image):
+        result = self.serialize_obj(image, 'Image', enum_modules=['azure.mgmt.compute.models'])
+        result.update({
+            'provisioning_state': image.provisioning_state,
+            'name': image.name,
+            'id': image.id,
+            'tags': image.tags or {}
+        })
+        return result
+
+    def resolve_storage_source(self, source):
+        blob_uri = None
+        disk = None
+        snapshot = None
+        if source.lower().endswith('.vhd'):
+            blob_uri = source
+            return (blob_uri, disk, snapshot)
+
+        tokenize = parse_resource_id(source)
+        if tokenize.get('type') == 'disks':
+            disk = source
+            return (blob_uri, disk, snapshot)
+
+        if tokenize.get('type') == 'snapshots':
+            snapshot = source
+            return (blob_uri, disk, snapshot)
+
+        # not a disk or snapshots
+        if 'type' in tokenize:
+            return (blob_uri, disk, snapshot)
+
+        # source can be name of snapshot or disk
+        snapshot_instance = self.get_snapshot(source)
+        if snapshot_instance:
+            snapshot = snapshot_instance.id
+            return (blob_uri, disk, snapshot)
+
+        disk_instance = self.get_disk(source)
+        if disk_instance:
+            disk = disk_instance.id
+        return (blob_uri, disk, snapshot)
+
+    def create_os_disk(self):
+        blob_uri, disk, snapshot = self.resolve_storage_source(self.source)
+        snapshot_resource = SubResource(snapshot) if snapshot else None
+        managed_disk = SubResource(disk) if disk else None
+        return ImageOSDisk(os_type=self.os_type,
+                           os_state=OperatingSystemStateTypes.generalized,
+                           snapshot=snapshot_resource,
+                           managed_disk=managed_disk,
+                           blob_uri=blob_uri)
+
+    def create_data_disk(self, lun, source):
+        blob_uri, disk, snapshot = self.resolve_storage_source(source)
+        if blob_uri or disk or snapshot:
+            snapshot_resource = SubResource(snapshot) if snapshot else None
+            managed_disk = SubResource(disk) if disk else None
+            return ImageDataDisk(lun,
+                                 blob_uri=blob_uri,
+                                 snapshot=snapshot_resource,
+                                 managed_disk=managed_disk)
+
+    def create_data_disks(self):
+        return list(filter(None, [self.create_data_disk(lun, source) for lun, source in enumerate(self.data_disk_sources)]))
+
+    def get_source_vm(self):
+        vm_resource_id = format_resource_id(self.source,
+                                            self.subscription_id,
+                                            'Microsoft.Compute',
+                                            'virtualMachines',
+                                            self.resource_group)
+        resource = parse_resource_id(vm_resource_id)
+        return self.get_vm(resource['resource_group'], resource['name']) if resource['type'] == 'virtualMachines' else None
+
+    def get_snapshot(self, snapshot_name):
+        try:
+            return self.compute_client.snapshots.get(self.resource_group, snapshot_name)
+        except Exception:
+            return None
+
+    def get_disk(self, disk_name):
+        try:
+            return self.compute_client.disks.get(self.resource_group, disk_name)
+        except Exception:
+            return None
+
+    def get_vm(self, resource_group, vm_name):
+        try:
+            return self.compute_client.virtual_machines.get(resource_group, vm_name, expand='instanceview')
+        except Exception:
+            return None
+
+    def get_image(self):
+        try:
+            return self.compute_client.images.get(self.resource_group, self.name)
+        except Exception:
+            return None
+
+    def create_image(self, image):
+        try:
+            poller = self.compute_client.images.create_or_update(self.resource_group, self.name, image)
+            new_image = self.get_poller_result(poller)
+        except Exception as exc:
+            self.fail("Error creating image {0} - {1}".format(self.name, str(exc)))
+        self.check_provisioning_state(new_image)
+        return new_image
+
+    def delete_image(self):
+        self.log('Deleting image {0}'.format(self.name))
+        try:
+            poller = self.compute_client.images.delete(self.resource_group, self.name)
+            result = self.get_poller_result(poller)
+        except Exception as exc:
+            self.fail("Error deleting image {0} - {1}".format(self.name, str(exc)))
+
+        return result
+
+
+def main():
+    AzureRMImage()
+
+if __name__ == '__main__':
+    main()
diff --git a/test/integration/targets/azure_rm_virtualmachine/tasks/virtualmachine.yml b/test/integration/targets/azure_rm_virtualmachine/tasks/virtualmachine.yml
index 3b1e8d2a3a2557..1f799544827f3f 100644
--- a/test/integration/targets/azure_rm_virtualmachine/tasks/virtualmachine.yml
+++ b/test/integration/targets/azure_rm_virtualmachine/tasks/virtualmachine.yml
@@ -130,6 +130,21 @@
 - assert:
       that: not output.changed
 
+- name: Create an image from VM
+  azure_rm_image:
+      resource_group: "{{ resource_group }}"
+      source: "https://{{ storage_account }}.blob.core.windows.net/testvm001/testvm001.vhd"
+      name: testimage001
+      os_type: Linux
+
+- name: Delete image
+  azure_rm_image:
+      resource_group: "{{ resource_group }}"
+      name: testimage01
+      source: "https://{{ storage_account }}.blob.core.windows.net/testvm001/testvm001.vhd"
+      os_type: Linux
+      state: absent
+
 - name: Delete VM
   azure_rm_virtualmachine:
       resource_group: "{{ resource_group }}"


Code review comments:
diff_hunk: @@ -0,0 +1,372 @@
+#!/usr/bin/python
+#
+# Copyright (c) 2017 Yuwei Zhou, <yuwzho@microsoft.com>
+#
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+                    'status': ['preview'],
+                    'supported_by': 'certified'}
+
+
+DOCUMENTATION = '''
+---
+module: azure_rm_image
+version_added: "2.5"
+short_description: Manage Azure image.
+description:
+    - Create, delete an image from virtual machine, blob uri, managed disk or snapshot.
+options:
+    resource_group:
+        description:
+            - Name of resource group.
+        required: true
+    name:
+        description:
+            - Name of the image.
+        required: true
+    source:
+        description:
+            - OS disk source from the same region, including a virtual machine id or name,
+              OS disk blob uri, managed OS disk id or name, or OS snapshot id or name.
+        required: true
+    data_disk_sources:
+        description:
+            - List of data disk sources, including unmanaged blob uri, managed disk id or name, or snapshot id or name.
+        required: false
+    location:
+        description:
+            - Location of the image.
+        required: false
+    os_type:
+        description: The OS type of image.
+        choices:
+            - Windows
+            - Linux
+    state:
+        description:
+            - Assert the state of the image. Use 'present' to create or update a image and 'absent' to delete an image.
+        required: false
+        default: present
+        choices:
+            - absent
+            - present
+
+extends_documentation_fragment:
+    - azure
+    - azure_tags
+
+author:
+    - "Yuwei Zhou (@yuwzho)"
+
+'''
+
+EXAMPLES = '''
+- name: Create an image from a virtual machine
+  azure_rm_image:
+    resource_group: Testing
+    name: foobar
+    source: testvm001
+
+- name: Create an image from os disk
+  azure_rm_image:
+    resource_group: Testing
+    name: foobar
+    source: /subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Compute/disks/disk001
+    data_disk_sources:
+        - datadisk001
+        - datadisk002
+    os_type: Linux
+
+- name: Delete an image
+  azure_rm_image:
+    state: absent
+    resource_group: Testing
+    name: foobar
+    source: testvm001
+'''
+
+RETURN = '''
+state:
+    description: Current state of the image.
+    returned: success
+    type: complex
+    contains:
+        id:
+          description: Image resource path.
+          type: str
+          example: "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Compute/images/foobar"
+        name:
+          description: Image name.
+          type: str
+          example: "foobar"
+        location:
+          description: Location of the image
+          type: str
+          example: "eastus"
+        resource_group:
+          description: Resource group of the image
+          type: str
+          example: "Testing"
+        provisioning_state:
+          description: Success or failure of the provisioning event.
+          type: str
+          example: "Succeeded"
+        properties:
+          description: Facts about the current image
+          type: complex
+          contains: {
+              "storageProfile": {
+                "osDisk": {
+                "storageAccountType": "Standard_LRS",
+                "managedDisk": {
+                    "id": "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Compute/disks/testvm001"
+                },
+                "diskSizeGB": 32,
+                "osState": "Generalized",
+                "caching": "ReadOnly",
+                "osType": "Linux"
+                },
+                "dataDisks": [
+                    {
+                        "caching": "ReadOnly",
+                        "diskSizeGB": 64,
+                        "storageAccountType": "Standard_LRS",
+                        "managedDisk": {
+                        "id": "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Compute/disks/testvm001-datadisk-0"
+                        },
+                        "lun": 0
+                    }
+                ]
+            },
+            "sourceVirtualMachine": {
+                "id": "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Compute/virtualMachines/testvm001"
+            }
+          }
+'''  # NOQA
+
+from ansible.module_utils.azure_rm_common import AzureRMModuleBase, format_resource_id
+
+try:
+    from msrestazure.tools import parse_resource_id
+    from msrestazure.azure_exceptions import CloudError
+    from azure.mgmt.compute.models import SubResource, OperatingSystemStateTypes, ImageStorageProfile, Image, ImageOSDisk, ImageDataDisk
+except ImportError:
+    # This is handled in azure_rm_common
+    pass
+
+
+class AzureRMImage(AzureRMModuleBase):
+
+    def __init__(self):
+
+        self.module_arg_spec = dict(
+            resource_group=dict(type='str', required=True),
+            name=dict(type='str', required=True),
+            state=dict(type='str', default='present', choices=['present', 'absent']),
+            location=dict(type='str'),
+            source=dict(type='str', required=True),
+            data_disk_sources=dict(type='list', default=[]),
+            os_type=dict(type='str', choices=['Windows', 'Linux'])
+        )
+
+        self.results = dict(
+            changed=False,
+            state=dict()
+        )
+
+        self.resource_group = None
+        self.name = None
+        self.state = None
+        self.location = None
+        self.source = None
+        self.data_disk_sources = None
+        self.os_type = None
+        self.tags = None
+
+        super(AzureRMImage, self).__init__(self.module_arg_spec, supports_check_mode=True)
+
+    def exec_module(self, **kwargs):
+
+        for key in self.module_arg_spec:
+            setattr(self, key, kwargs[key])
+
+        results = dict()
+        changed = False
+        image = None
+
+        if not self.location:
+            # Set default location
+            resource_group = self.get_resource_group(self.resource_group)
+            self.location = resource_group.location
+
+        self.log('Fetching image {0}'.format(self.name))
+        image = self.get_image()
+        if image:
+            self.check_provisioning_state(image, self.state)
+            results = self._image_to_dict(image)
+            # update is not supported
+            if self.state == 'absent':
+                changed = True
+        # the image does not exist and create a new one
+        elif self.state == 'present':
+            changed = True
+
+        self.results['changed'] = changed
+        self.results['state'] = results
+
+        if not self.check_mode and changed:
+
+            if self.state == 'present':
+                # create from virtual machine
+                vm = self.get_source_vm()
+                if vm:
+                    if self.data_disk_sources:
+                        self.fail('data_disk_sources is not allowed when capturing image from vm')
+                    image = Image(self.location, source_virtual_machine=SubResource(vm.id))
+                else:
+                    if not self.os_type:
+                        self.fail('os_type is required to create the image')
+                    os_disk = self.create_os_disk()
+                    data_disks = self.create_data_disks()
+                    storage_profile = ImageStorageProfile(os_disk=os_disk, data_disks=data_disks)
+                    image = Image(self.location, storage_profile=storage_profile, tags=self.tags)
+                new_image = self.create_image(image)
+                self.results['state'] = self._image_to_dict(new_image)
+
+            elif self.state == 'absent':
+                # delete image
+                self.delete_image()
+                # the delete does not actually return anything. if no exception, then we'll assume
+                # it worked.
+                self.results['state']['status'] = 'Deleted'
path: lib/ansible/modules/cloud/azure/azure_rm_image.py
text: @user1:
undocumented return value; should probably be removed or folded into a diff_mode impl

diff_hunk: @@ -0,0 +1,372 @@
+#!/usr/bin/python
+#
+# Copyright (c) 2017 Yuwei Zhou, <yuwzho@microsoft.com>
+#
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+                    'status': ['preview'],
+                    'supported_by': 'certified'}
+
+
+DOCUMENTATION = '''
+---
+module: azure_rm_image
+version_added: "2.5"
+short_description: Manage Azure image.
+description:
+    - Create, delete an image from virtual machine, blob uri, managed disk or snapshot.
+options:
+    resource_group:
+        description:
+            - Name of resource group.
+        required: true
+    name:
+        description:
+            - Name of the image.
+        required: true
+    source:
+        description:
+            - OS disk source from the same region, including a virtual machine id or name,
+              OS disk blob uri, managed OS disk id or name, or OS snapshot id or name.
+        required: true
+    data_disk_sources:
+        description:
+            - List of data disk sources, including unmanaged blob uri, managed disk id or name, or snapshot id or name.
+        required: false
path: lib/ansible/modules/cloud/azure/azure_rm_image.py
text: @user1:
`required: false` is not necessary and can be removed from all these

diff_hunk: @@ -0,0 +1,372 @@
+#!/usr/bin/python
+#
+# Copyright (c) 2017 Yuwei Zhou, <yuwzho@microsoft.com>
+#
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+                    'status': ['preview'],
+                    'supported_by': 'certified'}
+
+
+DOCUMENTATION = '''
+---
+module: azure_rm_image
+version_added: "2.5"
+short_description: Manage Azure image.
+description:
+    - Create, delete an image from virtual machine, blob uri, managed disk or snapshot.
+options:
+    resource_group:
+        description:
+            - Name of resource group.
+        required: true
+    name:
+        description:
+            - Name of the image.
+        required: true
+    source:
+        description:
+            - OS disk source from the same region, including a virtual machine id or name,
+              OS disk blob uri, managed OS disk id or name, or OS snapshot id or name.
+        required: true
+    data_disk_sources:
+        description:
+            - List of data disk sources, including unmanaged blob uri, managed disk id or name, or snapshot id or name.
+        required: false
+    location:
+        description:
+            - Location of the image.
+        required: false
+    os_type:
+        description: The OS type of image.
+        choices:
+            - Windows
+            - Linux
+    state:
+        description:
+            - Assert the state of the image. Use 'present' to create or update a image and 'absent' to delete an image.
+        required: false
+        default: present
+        choices:
+            - absent
+            - present
+
+extends_documentation_fragment:
+    - azure
+    - azure_tags
+
+author:
+    - "Yuwei Zhou (@yuwzho)"
+
+'''
+
+EXAMPLES = '''
+- name: Create an image from a virtual machine
+  azure_rm_image:
+    resource_group: Testing
+    name: foobar
+    source: testvm001
+
+- name: Create an image from os disk
+  azure_rm_image:
+    resource_group: Testing
+    name: foobar
+    source: /subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Compute/disks/disk001
+    data_disk_sources:
+        - datadisk001
+        - datadisk002
+    os_type: Linux
+
+- name: Delete an image
+  azure_rm_image:
+    state: absent
+    resource_group: Testing
+    name: foobar
+    source: testvm001
+'''
+
+RETURN = '''
+state:
+    description: Current state of the image.
+    returned: success
+    type: complex
+    contains:
+        id:
+          description: Image resource path.
+          type: str
+          example: "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Compute/images/foobar"
+        name:
+          description: Image name.
+          type: str
+          example: "foobar"
+        location:
+          description: Location of the image
+          type: str
+          example: "eastus"
+        resource_group:
+          description: Resource group of the image
+          type: str
+          example: "Testing"
+        provisioning_state:
+          description: Success or failure of the provisioning event.
+          type: str
+          example: "Succeeded"
+        properties:
+          description: Facts about the current image
+          type: complex
+          contains: {
+              "storageProfile": {
+                "osDisk": {
+                "storageAccountType": "Standard_LRS",
+                "managedDisk": {
+                    "id": "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Compute/disks/testvm001"
+                },
+                "diskSizeGB": 32,
+                "osState": "Generalized",
+                "caching": "ReadOnly",
+                "osType": "Linux"
+                },
+                "dataDisks": [
+                    {
+                        "caching": "ReadOnly",
+                        "diskSizeGB": 64,
+                        "storageAccountType": "Standard_LRS",
+                        "managedDisk": {
+                        "id": "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Compute/disks/testvm001-datadisk-0"
+                        },
+                        "lun": 0
+                    }
+                ]
+            },
+            "sourceVirtualMachine": {
+                "id": "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Compute/virtualMachines/testvm001"
+            }
+          }
+'''  # NOQA
+
+from ansible.module_utils.azure_rm_common import AzureRMModuleBase, format_resource_id
+
+try:
+    from msrestazure.tools import parse_resource_id
+    from msrestazure.azure_exceptions import CloudError
+    from azure.mgmt.compute.models import SubResource, OperatingSystemStateTypes, ImageStorageProfile, Image, ImageOSDisk, ImageDataDisk
+except ImportError:
+    # This is handled in azure_rm_common
+    pass
+
+
+class AzureRMImage(AzureRMModuleBase):
+
+    def __init__(self):
+
+        self.module_arg_spec = dict(
+            resource_group=dict(type='str', required=True),
+            name=dict(type='str', required=True),
+            state=dict(type='str', default='present', choices=['present', 'absent']),
+            location=dict(type='str'),
+            source=dict(type='str', required=True),
+            data_disk_sources=dict(type='list', default=[]),
+            os_type=dict(type='str', choices=['Windows', 'Linux'])
+        )
+
+        self.results = dict(
+            changed=False,
+            state=dict()
+        )
+
+        self.resource_group = None
+        self.name = None
+        self.state = None
+        self.location = None
+        self.source = None
+        self.data_disk_sources = None
+        self.os_type = None
+        self.tags = None
+
+        super(AzureRMImage, self).__init__(self.module_arg_spec, supports_check_mode=True)
+
+    def exec_module(self, **kwargs):
+
+        for key in self.module_arg_spec:
+            setattr(self, key, kwargs[key])
+
+        results = dict()
+        changed = False
+        image = None
+
+        if not self.location:
+            # Set default location
+            resource_group = self.get_resource_group(self.resource_group)
+            self.location = resource_group.location
+
+        self.log('Fetching image {0}'.format(self.name))
+        image = self.get_image()
+        if image:
+            self.check_provisioning_state(image, self.state)
+            results = self._image_to_dict(image)
+            # update is not supported
+            if self.state == 'absent':
+                changed = True
+        # the image does not exist and create a new one
+        elif self.state == 'present':
+            changed = True
+
+        self.results['changed'] = changed
+        self.results['state'] = results
+
+        if not self.check_mode and changed:
+
+            if self.state == 'present':
+                # create from virtual machine
+                vm = self.get_source_vm()
+                if vm:
+                    if self.data_disk_sources:
+                        self.fail('data_disk_sources is not allowed when capturing image from vm')
+                    image = Image(self.location, source_virtual_machine=SubResource(vm.id))
+                else:
+                    if not self.os_type:
+                        self.fail('os_type is required to create the image')
+                    os_disk = self.create_os_disk()
+                    data_disks = self.create_data_disks()
+                    storage_profile = ImageStorageProfile(os_disk=os_disk, data_disks=data_disks)
+                    image = Image(self.location, storage_profile=storage_profile, tags=self.tags)
+                new_image = self.create_image(image)
+                self.results['state'] = self._image_to_dict(new_image)
+
+            elif self.state == 'absent':
+                # delete image
+                self.delete_image()
+                # the delete does not actually return anything. if no exception, then we'll assume
+                # it worked.
+                self.results['state']['status'] = 'Deleted'
+
+        return self.results
+
+    def _image_to_dict(self, image):
+        result = self.serialize_obj(image, 'Image', enum_modules=['azure.mgmt.compute.models'])
+        result.update({
+            'provisioning_state': image.provisioning_state,
path: lib/ansible/modules/cloud/azure/azure_rm_image.py
text: @user1:
I think we've fixed the underlying bug that necessitates this update (the model/msrest serializer change around not serializing `readonly` model values), so this should no longer be necessary

@author:
Actually the `self.serialize_obj` has not been fixed, it still missing these field. But `as_dict` is perfect

diff_hunk: @@ -0,0 +1,372 @@
+#!/usr/bin/python
+#
+# Copyright (c) 2017 Yuwei Zhou, <yuwzho@microsoft.com>
+#
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+                    'status': ['preview'],
+                    'supported_by': 'certified'}
+
+
+DOCUMENTATION = '''
+---
+module: azure_rm_image
+version_added: "2.5"
+short_description: Manage Azure image.
+description:
+    - Create, delete an image from virtual machine, blob uri, managed disk or snapshot.
+options:
+    resource_group:
+        description:
+            - Name of resource group.
+        required: true
+    name:
+        description:
+            - Name of the image.
+        required: true
+    source:
+        description:
+            - OS disk source from the same region, including a virtual machine id or name,
+              OS disk blob uri, managed OS disk id or name, or OS snapshot id or name.
+        required: true
+    data_disk_sources:
+        description:
+            - List of data disk sources, including unmanaged blob uri, managed disk id or name, or snapshot id or name.
+        required: false
+    location:
+        description:
+            - Location of the image.
+        required: false
+    os_type:
+        description: The OS type of image.
+        choices:
+            - Windows
+            - Linux
+    state:
+        description:
+            - Assert the state of the image. Use 'present' to create or update a image and 'absent' to delete an image.
+        required: false
+        default: present
+        choices:
+            - absent
+            - present
+
+extends_documentation_fragment:
+    - azure
+    - azure_tags
+
+author:
+    - "Yuwei Zhou (@yuwzho)"
+
+'''
+
+EXAMPLES = '''
+- name: Create an image from a virtual machine
+  azure_rm_image:
+    resource_group: Testing
+    name: foobar
+    source: testvm001
+
+- name: Create an image from os disk
+  azure_rm_image:
+    resource_group: Testing
+    name: foobar
+    source: /subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Compute/disks/disk001
+    data_disk_sources:
+        - datadisk001
+        - datadisk002
+    os_type: Linux
+
+- name: Delete an image
+  azure_rm_image:
+    state: absent
+    resource_group: Testing
+    name: foobar
+    source: testvm001
+'''
+
+RETURN = '''
+state:
+    description: Current state of the image.
+    returned: success
+    type: complex
+    contains:
+        id:
+          description: Image resource path.
+          type: str
+          example: "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Compute/images/foobar"
+        name:
+          description: Image name.
+          type: str
+          example: "foobar"
+        location:
+          description: Location of the image
+          type: str
+          example: "eastus"
+        resource_group:
+          description: Resource group of the image
+          type: str
+          example: "Testing"
+        provisioning_state:
+          description: Success or failure of the provisioning event.
+          type: str
+          example: "Succeeded"
+        properties:
+          description: Facts about the current image
+          type: complex
+          contains: {
+              "storageProfile": {
+                "osDisk": {
+                "storageAccountType": "Standard_LRS",
+                "managedDisk": {
+                    "id": "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Compute/disks/testvm001"
+                },
+                "diskSizeGB": 32,
+                "osState": "Generalized",
+                "caching": "ReadOnly",
+                "osType": "Linux"
+                },
+                "dataDisks": [
+                    {
+                        "caching": "ReadOnly",
+                        "diskSizeGB": 64,
+                        "storageAccountType": "Standard_LRS",
+                        "managedDisk": {
+                        "id": "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Compute/disks/testvm001-datadisk-0"
+                        },
+                        "lun": 0
+                    }
+                ]
+            },
+            "sourceVirtualMachine": {
+                "id": "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Compute/virtualMachines/testvm001"
+            }
+          }
+'''  # NOQA
+
+from ansible.module_utils.azure_rm_common import AzureRMModuleBase, format_resource_id
+
+try:
+    from msrestazure.tools import parse_resource_id
+    from msrestazure.azure_exceptions import CloudError
+    from azure.mgmt.compute.models import SubResource, OperatingSystemStateTypes, ImageStorageProfile, Image, ImageOSDisk, ImageDataDisk
+except ImportError:
+    # This is handled in azure_rm_common
+    pass
+
+
+class AzureRMImage(AzureRMModuleBase):
+
+    def __init__(self):
+
+        self.module_arg_spec = dict(
+            resource_group=dict(type='str', required=True),
+            name=dict(type='str', required=True),
+            state=dict(type='str', default='present', choices=['present', 'absent']),
+            location=dict(type='str'),
+            source=dict(type='str', required=True),
+            data_disk_sources=dict(type='list', default=[]),
+            os_type=dict(type='str', choices=['Windows', 'Linux'])
+        )
+
+        self.results = dict(
+            changed=False,
+            state=dict()
+        )
+
+        self.resource_group = None
+        self.name = None
+        self.state = None
+        self.location = None
+        self.source = None
+        self.data_disk_sources = None
+        self.os_type = None
+        self.tags = None
+
+        super(AzureRMImage, self).__init__(self.module_arg_spec, supports_check_mode=True)
+
+    def exec_module(self, **kwargs):
+
+        for key in self.module_arg_spec:
+            setattr(self, key, kwargs[key])
+
+        results = dict()
+        changed = False
+        image = None
+
+        if not self.location:
+            # Set default location
+            resource_group = self.get_resource_group(self.resource_group)
+            self.location = resource_group.location
+
+        self.log('Fetching image {0}'.format(self.name))
+        image = self.get_image()
+        if image:
+            self.check_provisioning_state(image, self.state)
+            results = self._image_to_dict(image)
+            # update is not supported
+            if self.state == 'absent':
+                changed = True
+        # the image does not exist and create a new one
+        elif self.state == 'present':
+            changed = True
+
+        self.results['changed'] = changed
+        self.results['state'] = results
+
+        if not self.check_mode and changed:
+
+            if self.state == 'present':
+                # create from virtual machine
+                vm = self.get_source_vm()
+                if vm:
+                    if self.data_disk_sources:
+                        self.fail('data_disk_sources is not allowed when capturing image from vm')
+                    image = Image(self.location, source_virtual_machine=SubResource(vm.id))
+                else:
+                    if not self.os_type:
+                        self.fail('os_type is required to create the image')
+                    os_disk = self.create_os_disk()
+                    data_disks = self.create_data_disks()
+                    storage_profile = ImageStorageProfile(os_disk=os_disk, data_disks=data_disks)
+                    image = Image(self.location, storage_profile=storage_profile, tags=self.tags)
+                new_image = self.create_image(image)
+                self.results['state'] = self._image_to_dict(new_image)
+
+            elif self.state == 'absent':
+                # delete image
+                self.delete_image()
+                # the delete does not actually return anything. if no exception, then we'll assume
+                # it worked.
+                self.results['state']['status'] = 'Deleted'
+
+        return self.results
+
+    def _image_to_dict(self, image):
+        result = self.serialize_obj(image, 'Image', enum_modules=['azure.mgmt.compute.models'])
+        result.update({
+            'provisioning_state': image.provisioning_state,
+            'name': image.name,
+            'id': image.id,
+            'tags': image.tags or {}
+        })
+        return result
+
+    def resolve_storage_source(self, source):
+        blob_uri = None
+        disk = None
+        snapshot = None
+        if source.lower().endswith('.vhd'):
+            blob_uri = source
+            return (blob_uri, disk, snapshot)
+
+        tokenize = parse_resource_id(source)
+        if tokenize.get('type') == 'disks':
+            disk = source
+            return (blob_uri, disk, snapshot)
+
+        if tokenize.get('type') == 'snapshots':
+            snapshot = source
+            return (blob_uri, disk, snapshot)
+
+        # not a disk or snapshots
+        if 'type' in tokenize:
+            return (blob_uri, disk, snapshot)
+
+        # source can be name of snapshot or disk
+        snapshot_instance = self.get_snapshot(source)
+        if snapshot_instance:
+            snapshot = snapshot_instance.id
+            return (blob_uri, disk, snapshot)
+
+        disk_instance = self.get_disk(source)
+        if disk_instance:
+            disk = disk_instance.id
+        return (blob_uri, disk, snapshot)
+
+    def create_os_disk(self):
+        blob_uri, disk, snapshot = self.resolve_storage_source(self.source)
+        snapshot_resource = SubResource(snapshot) if snapshot else None
+        managed_disk = SubResource(disk) if disk else None
+        return ImageOSDisk(os_type=self.os_type,
+                           os_state=OperatingSystemStateTypes.generalized,
+                           snapshot=snapshot_resource,
+                           managed_disk=managed_disk,
+                           blob_uri=blob_uri)
+
+    def create_data_disk(self, lun, source):
+        blob_uri, disk, snapshot = self.resolve_storage_source(source)
+        if blob_uri or disk or snapshot:
+            snapshot_resource = SubResource(snapshot) if snapshot else None
+            managed_disk = SubResource(disk) if disk else None
+            return ImageDataDisk(lun,
+                                 blob_uri=blob_uri,
+                                 snapshot=snapshot_resource,
+                                 managed_disk=managed_disk)
+
+    def create_data_disks(self):
+        return list(filter(None, [self.create_data_disk(lun, source) for lun, source in enumerate(self.data_disk_sources)]))
+
+    def get_source_vm(self):
+        vm_resource_id = format_resource_id(self.source,
+                                            self.subscription_id,
+                                            'Microsoft.Compute',
+                                            'virtualMachines',
+                                            self.resource_group)
+        resource = parse_resource_id(vm_resource_id)
+        return self.get_vm(resource['resource_group'], resource['name']) if resource['type'] == 'virtualMachines' else None
+
+    def get_snapshot(self, snapshot_name):
+        try:
+            return self.compute_client.snapshots.get(self.resource_group, snapshot_name)
+        except Exception:
path: lib/ansible/modules/cloud/azure/azure_rm_image.py
text: @user1:
Blanket exception-swallowing like this is dangerous and could mask a number of other issues... Should probably just catch `CloudError` and check if `error.error` is `ResourceNotFound`- if it's not, `raise` and be done.

@author:
It should check the `CloudError.error.error` is `ResourceNotFound`. I check the `status_code == 404` instead