This post describes what and where needs to be added in order to create a new module.
File Locations
Let’s say you want to create modules for a new Azure resource. You will need two modules - main module and facts module.
Following files should be created in Ansible directory:
Please note that integration tests can be shared (and in many cases should be shared) by multiple modules.
Typical Module Dissected
Header
Module header is always the same. Only parts that are changed are:
- copyright year
- original contributor name
- original contributor git handle
12345678910111213
#!/usr/bin/python## Copyright (c) 2019 John Doe, (@johndoe)## GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)from__future__importabsolute_import,division,print_function__metaclass__=typeANSIBLE_METADATA={'metadata_version':'1.1','status':['preview'],'supported_by':'community'}
DOCUMENTATION='''---module: azure_rm_devtestlabvirtualmachineversion_added: "2.8"short_description: Manage Azure DevTest Lab Virtual Machine instance.description: - Create, update and delete instance of Azure DevTest Lab Virtual Machine.options: resource_group: description: - The name of the resource group. required: True lab_name: description: - The name of the lab. required: True name: description: - The name of the virtual machine. required: True notes: description: - The notes of the virtual machine. state: description: - Assert the state of the Virtual Machine. - Use 'present' to create or update an Virtual Machine and 'absent' to delete it. default: present choices: - absent - presentextends_documentation_fragment: - azure - azure_tagsauthor: - "Zim Kalinowski (@zikalino)"'''
Return value should contain detailed specification of return value in YAML format.
Please note that the main module should:
- contain resource id
- contain information that is essential for subsequent use of the resource (for instance FQDN, IP address, etc.)
- shouldn’t contain information that is supplied via input parameters
1234567891011121314151617181920
RETURN='''id: description: - The identifier of the DTL Virtual Machine resource. returned: always type: str sample: /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourcegroups/myrg/providers/microsoft.devtestlab/labs/mylab/virtualmachines/myvmcompute_id: description: - The identifier of the underlying Compute Virtual Machine resource. returned: always type: str sample: /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourcegroups/myrg/providers/microsoft.devtestlab/labs/mylab/virtualmachines/myvmfqdn: description: - Fully qualified domain name or IP Address of the virtual machine. returned: always type: str sample: myvm.eastus.cloudapp.azure.com'''
Imports
Usually very similar, may contain additional imports related to particular Azure resource, etc.
12345678910111213
importtimefromansible.module_utils.azure_rm_commonimportAzureRMModuleBasefromansible.module_utils.common.dict_transformationsimport_snake_to_cameltry:frommsrestazure.azure_exceptionsimportCloudErrorfrommsrest.pollingimportLROPollerfrommsrestazure.azure_operationimportAzureOperationPollerfromazure.mgmt.devtestlabsimportDevTestLabsClientfrommsrest.serializationimportModelexceptImportError:# This is handled in azure_rm_commonpass
self.lab_virtual_machine['gallery_image_reference']=self.lab_virtual_machine.pop('image',None)ifself.lab_virtual_machine.get('artifacts')isnotNone:forartifactinself.lab_virtual_machine.get('artifacts'):source_name=artifact.pop('source_name')source_path=artifact.pop('source_path')template="/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.DevTestLab/labs/{2}/artifactsources/{3}{4}"artifact['artifact_id']=template.format(self.subscription_id,self.resource_group,self.lab_name,source_name,source_path)self.lab_virtual_machine['size']=self.lab_virtual_machine.pop('vm_size')self.lab_virtual_machine['os_type']=_snake_to_camel(self.lab_virtual_machine['os_type'],True)ifself.lab_virtual_machine.get('storage_type'):self.lab_virtual_machine['storage_type']=_snake_to_camel(self.lab_virtual_machine['storage_type'],True)lab_subnet=self.lab_virtual_machine.pop('lab_subnet')ifisinstance(lab_subnet,str):vn_and_subnet=lab_subnet.split('/subnets/')if(len(vn_and_subnet)==2):self.lab_virtual_machine['lab_virtual_network_id']=vn_and_subnet[0]self.lab_virtual_machine['lab_subnet_name']=vn_and_subnet[1]else:self.fail("Invalid 'lab_subnet' resource id format")else:template="/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.DevTestLab/labs/{2}/virtualnetworks/{3}"self.lab_virtual_machine['lab_virtual_network_id']=template.format(self.subscription_id,self.resource_group,self.lab_name,lab_subnet.get('virtual_network_name'))self.lab_virtual_machine['lab_subnet_name']=lab_subnet.get('name')
old_response=self.get_virtualmachine()ifnotold_response:self.log("Virtual Machine instance doesn't exist")ifself.state=='absent':self.log("Old instance didn't exist")else:self.to_do=Actions.Create# get location from the lab as it has to be the same and has to be specified (why??)lab=self.get_devtestlab()self.lab_virtual_machine['location']=lab['location']else:self.log("Virtual Machine instance already exists")ifself.state=='absent':self.to_do=Actions.Deleteelifself.state=='present':self.lab_virtual_machine['location']=old_response['location']ifold_response['size'].lower()!=self.lab_virtual_machine.get('size').lower():self.lab_virtual_machine['size']=old_response['size']self.module.warn("Property 'size' cannot be changed")ifself.lab_virtual_machine.get('storage_type')isnotNoneand \
old_response['storage_type'].lower()!=self.lab_virtual_machine.get('storage_type').lower():self.lab_virtual_machine['storage_type']=old_response['storage_type']self.module.warn("Property 'storage_type' cannot be changed")ifold_response.get('gallery_image_reference',{})!=self.lab_virtual_machine.get('gallery_image_reference',{}):self.lab_virtual_machine['gallery_image_reference']=old_response['gallery_image_reference']self.module.warn("Property 'image' cannot be changed")# currently artifacts can be only specified when vm is created# and in addition we don't have detailed information, just a number of "total artifacts"iflen(self.lab_virtual_machine.get('artifacts',[]))!=old_response['artifact_deployment_status']['total_artifacts']:self.module.warn("Property 'artifacts' cannot be changed")ifself.lab_virtual_machine.get('disallow_public_ip_address')isnotNone:ifold_response['disallow_public_ip_address']!=self.lab_virtual_machine.get('disallow_public_ip_address'):self.module.warn("Property 'disallow_public_ip_address' cannot be changed")self.lab_virtual_machine['disallow_public_ip_address']=old_response['disallow_public_ip_address']ifself.lab_virtual_machine.get('allow_claim')isnotNone:ifold_response['allow_claim']!=self.lab_virtual_machine.get('allow_claim'):self.module.warn("Property 'allow_claim' cannot be changed")self.lab_virtual_machine['allow_claim']=old_response['allow_claim']ifself.lab_virtual_machine.get('notes')isnotNone:ifold_response['notes']!=self.lab_virtual_machine.get('notes'):self.to_do=Actions.Updateelse:self.lab_virtual_machine['notes']=old_response['notes']
Performing Desired Action
12345678910111213141516171819202122
if(self.to_do==Actions.Create)or(self.to_do==Actions.Update):self.log("Need to Create / Update the Virtual Machine instance")self.results['changed']=Trueifself.check_mode:returnself.resultsresponse=self.create_update_virtualmachine()self.log("Creation / Update done")elifself.to_do==Actions.Delete:self.log("Virtual Machine instance deleted")self.results['changed']=Trueifself.check_mode:returnself.resultsself.delete_virtualmachine()else:self.log("Virtual Machine instance unchanged")self.results['changed']=Falseresponse=old_response
defcreate_update_virtualmachine(self):''' Creates or updates Virtual Machine with the specified configuration. :return: deserialized Virtual Machine instance state dictionary '''self.log("Creating / Updating the Virtual Machine instance {0}".format(self.name))try:response=self.mgmt_client.virtual_machines.create_or_update(resource_group_name=self.resource_group,lab_name=self.lab_name,name=self.name,lab_virtual_machine=self.lab_virtual_machine)ifisinstance(response,LROPoller)orisinstance(response,AzureOperationPoller):response=self.get_poller_result(response)exceptCloudErrorasexc:self.log('Error attempting to create the Virtual Machine instance.')self.fail("Error creating the Virtual Machine instance: {0}".format(str(exc)))returnresponse.as_dict()
Delete Function
12345678910111213141516171819
defdelete_virtualmachine(self):''' Deletes specified Virtual Machine instance in the specified subscription and resource group. :return: True '''self.log("Deleting the Virtual Machine instance {0}".format(self.name))try:response=self.mgmt_client.virtual_machines.delete(resource_group_name=self.resource_group,lab_name=self.lab_name,name=self.name)exceptCloudErrorase:self.log('Error attempting to delete the Virtual Machine instance.')self.fail("Error deleting the Virtual Machine instance: {0}".format(str(e)))ifisinstance(response,LROPoller)orisinstance(response,AzureOperationPoller):response=self.get_poller_result(response)returnTrue
Get Function
123456789101112131415161718192021
defget_virtualmachine(self):''' Gets the properties of the specified Virtual Machine. :return: deserialized Virtual Machine instance state dictionary '''self.log("Checking if the Virtual Machine instance {0} is present".format(self.name))found=Falsetry:response=self.mgmt_client.virtual_machines.get(resource_group_name=self.resource_group,lab_name=self.lab_name,name=self.name)found=Trueself.log("Response : {0}".format(response))self.log("Virtual Machine instance : {0} found".format(response.name))exceptCloudErrorase:self.log('Did not find the Virtual Machine instance.')iffoundisTrue:returnresponse.as_dict()returnFalse