Update and pin to azure-2.0.0rc5
This commit is contained in:
parent
992b999bde
commit
9c6da9194a
14 changed files with 224 additions and 206 deletions
|
@ -248,7 +248,7 @@ def nic_to_dict(nic):
|
|||
ip_configuration=dict(
|
||||
name=nic.ip_configurations[0].name,
|
||||
private_ip_address=nic.ip_configurations[0].private_ip_address,
|
||||
private_ip_allocation_method=nic.ip_configurations[0].private_ip_allocation_method.value,
|
||||
private_ip_allocation_method=nic.ip_configurations[0].private_ip_allocation_method,
|
||||
subnet=dict(),
|
||||
public_ip_address=dict(),
|
||||
),
|
||||
|
@ -460,18 +460,17 @@ class AzureRMNetworkInterface(AzureRMModuleBase):
|
|||
|
||||
nic = NetworkInterface(
|
||||
location=self.location,
|
||||
name=self.name,
|
||||
tags=self.tags,
|
||||
ip_configurations=[
|
||||
NetworkInterfaceIPConfiguration(
|
||||
name='default',
|
||||
private_ip_allocation_method=self.private_ip_allocation_method,
|
||||
)
|
||||
]
|
||||
)
|
||||
#nic.name = self.name
|
||||
nic.ip_configurations[0].subnet = Subnet(id=subnet.id)
|
||||
nic.ip_configurations[0].name = 'default'
|
||||
nic.network_security_group = NetworkSecurityGroup(id=nsg.id,
|
||||
name=nsg.name,
|
||||
location=nsg.location,
|
||||
resource_guid=nsg.resource_guid)
|
||||
if self.private_ip_address:
|
||||
|
@ -480,26 +479,26 @@ class AzureRMNetworkInterface(AzureRMModuleBase):
|
|||
if pip:
|
||||
nic.ip_configurations[0].public_ip_address = PublicIPAddress(
|
||||
id=pip.id,
|
||||
name=pip.name,
|
||||
location=pip.location,
|
||||
resource_guid=pip.resource_guid)
|
||||
else:
|
||||
self.log("Updating network interface {0}.".format(self.name))
|
||||
nic = NetworkInterface(
|
||||
id=results['id'],
|
||||
location=results['location'],
|
||||
name=results['name'],
|
||||
tags=results['tags'],
|
||||
ip_configurations=[
|
||||
NetworkInterfaceIPConfiguration(
|
||||
name=results['ip_configuration']['name'],
|
||||
private_ip_allocation_method=
|
||||
results['ip_configuration']['private_ip_allocation_method'],
|
||||
results['ip_configuration']['private_ip_allocation_method']
|
||||
)
|
||||
],
|
||||
]
|
||||
)
|
||||
subnet = self.get_subnet(results['ip_configuration']['subnet']['virtual_network_name'],
|
||||
results['ip_configuration']['subnet']['name'])
|
||||
nic.ip_configurations[0].subnet = Subnet(id=subnet.id)
|
||||
nic.ip_configurations[0].name = results['ip_configuration']['name']
|
||||
#nic.name = name=results['name'],
|
||||
|
||||
if results['ip_configuration'].get('private_ip_address'):
|
||||
nic.ip_configurations[0].private_ip_address = results['ip_configuration']['private_ip_address']
|
||||
|
@ -509,14 +508,13 @@ class AzureRMNetworkInterface(AzureRMModuleBase):
|
|||
self.get_public_ip_address(results['ip_configuration']['public_ip_address']['name'])
|
||||
nic.ip_configurations[0].public_ip_address = PublicIPAddress(
|
||||
id=pip.id,
|
||||
name=pip.name,
|
||||
location=pip.location,
|
||||
resource_guid=pip.resource_guid)
|
||||
#name=pip.name,
|
||||
|
||||
if results['network_security_group'].get('id'):
|
||||
nsg = self.get_security_group(results['network_security_group']['name'])
|
||||
nic.network_security_group = NetworkSecurityGroup(id=nsg.id,
|
||||
name=nsg.name,
|
||||
location=nsg.location,
|
||||
resource_guid=nsg.resource_guid)
|
||||
|
||||
|
@ -539,15 +537,6 @@ class AzureRMNetworkInterface(AzureRMModuleBase):
|
|||
except Exception as exc:
|
||||
self.fail("Error creating or updating network interface {0} - {1}".format(self.name, str(exc)))
|
||||
|
||||
self.log("new_nic:")
|
||||
self.log(new_nic)
|
||||
self.log(new_nic.network_security_group)
|
||||
|
||||
if len(new_nic.ip_configurations) > 0:
|
||||
for config in new_nic.ip_configurations:
|
||||
self.log("ip configurations")
|
||||
self.log(config)
|
||||
|
||||
return nic_to_dict(new_nic)
|
||||
|
||||
def delete_nic(self):
|
||||
|
|
|
@ -75,16 +75,11 @@ EXAMPLES = '''
|
|||
'''
|
||||
|
||||
RETURN = '''
|
||||
changed:
|
||||
description: Whether or not the object was changed.
|
||||
returned: always
|
||||
type: bool
|
||||
sample: False
|
||||
objects:
|
||||
description: List containing a set of facts for each selected object.
|
||||
azure_networkinterfaces:
|
||||
description: List of network interface dicts.
|
||||
returned: always
|
||||
type: list
|
||||
sample: [{
|
||||
example: [{
|
||||
"dns_settings": {
|
||||
"applied_dns_servers": [],
|
||||
"dns_servers": [],
|
||||
|
@ -148,7 +143,7 @@ class AzureRMNetworkInterfaceFacts(AzureRMModuleBase):
|
|||
|
||||
self.results = dict(
|
||||
changed=False,
|
||||
objects=[]
|
||||
ansible_facts=dict(azure_networkinterfaces=[])
|
||||
)
|
||||
|
||||
self.name = None
|
||||
|
@ -169,11 +164,11 @@ class AzureRMNetworkInterfaceFacts(AzureRMModuleBase):
|
|||
self.fail("Parameter error: resource group required when filtering by name.")
|
||||
|
||||
if self.name:
|
||||
self.results['objects'] = self.get_item()
|
||||
self.results['ansible_facts']['azure_networkinterfaces'] = self.get_item()
|
||||
elif self.resource_group:
|
||||
self.results['objects'] = self.list_resource_group()
|
||||
self.results['ansible_facts']['azure_networkinterfaces'] = self.list_resource_group()
|
||||
else:
|
||||
self.results['objects'] = self.list_all()
|
||||
self.results['ansible_facts']['azure_networkinterfaces'] = self.list_all()
|
||||
|
||||
return self.results
|
||||
|
||||
|
@ -187,7 +182,8 @@ class AzureRMNetworkInterfaceFacts(AzureRMModuleBase):
|
|||
pass
|
||||
|
||||
if item and self.has_tags(item.tags, self.tags):
|
||||
result = [self.serialize_obj(item, AZURE_OBJECT_CLASS)]
|
||||
nic = self.serialize_obj(item, AZURE_OBJECT_CLASS)
|
||||
result = [nic]
|
||||
|
||||
return result
|
||||
|
||||
|
@ -201,7 +197,8 @@ class AzureRMNetworkInterfaceFacts(AzureRMModuleBase):
|
|||
results = []
|
||||
for item in response:
|
||||
if self.has_tags(item.tags, self.tags):
|
||||
results.append(self.serialize_obj(item, AZURE_OBJECT_CLASS))
|
||||
nic = self.serialize_obj(item, AZURE_OBJECT_CLASS)
|
||||
results.append(nic)
|
||||
return results
|
||||
|
||||
def list_all(self):
|
||||
|
@ -214,7 +211,8 @@ class AzureRMNetworkInterfaceFacts(AzureRMModuleBase):
|
|||
results = []
|
||||
for item in response:
|
||||
if self.has_tags(item.tags, self.tags):
|
||||
results.append(self.serialize_obj(item, AZURE_OBJECT_CLASS))
|
||||
nic = self.serialize_obj(item, AZURE_OBJECT_CLASS)
|
||||
results.append(nic)
|
||||
return results
|
||||
|
||||
|
||||
|
|
|
@ -134,7 +134,7 @@ def pip_to_dict(pip):
|
|||
type=pip.type,
|
||||
location=pip.location,
|
||||
tags=pip.tags,
|
||||
public_ip_allocation_method=pip.public_ip_allocation_method.value,
|
||||
public_ip_allocation_method=pip.public_ip_allocation_method,
|
||||
dns_settings=dict(),
|
||||
ip_address=pip.ip_address,
|
||||
idle_timeout_in_minutes=pip.idle_timeout_in_minutes,
|
||||
|
|
|
@ -68,16 +68,11 @@ EXAMPLES = '''
|
|||
'''
|
||||
|
||||
RETURN = '''
|
||||
changed:
|
||||
description: Whether or not the object was changed.
|
||||
returned: always
|
||||
type: bool
|
||||
sample: False
|
||||
objects:
|
||||
description: List containing a set of facts for each selected object.
|
||||
azure_publicipaddresses:
|
||||
description: List of public IP address dicts.
|
||||
returned: always
|
||||
type: list
|
||||
sample: [{
|
||||
example: [{
|
||||
"etag": 'W/"a31a6d7d-cb18-40a5-b16d-9f4a36c1b18a"',
|
||||
"id": "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Network/publicIPAddresses/pip2001",
|
||||
"location": "eastus2",
|
||||
|
@ -112,13 +107,13 @@ class AzureRMPublicIPFacts(AzureRMModuleBase):
|
|||
|
||||
self.module_arg_spec = dict(
|
||||
name=dict(type='str'),
|
||||
resource_group=dict(required=True, type='str'),
|
||||
resource_group=dict(type='str'),
|
||||
tags=dict(type='list')
|
||||
)
|
||||
|
||||
self.results = dict(
|
||||
changed=False,
|
||||
objects=[]
|
||||
ansible_facts=dict(azure_publicipaddresses=[])
|
||||
)
|
||||
|
||||
self.name = None
|
||||
|
@ -138,11 +133,11 @@ class AzureRMPublicIPFacts(AzureRMModuleBase):
|
|||
self.fail("Parameter error: resource group required when filtering by name.")
|
||||
|
||||
if self.name:
|
||||
self.results['objects'] = self.get_item()
|
||||
self.results['ansible_facts']['azure_publicipaddresses'] = self.get_item()
|
||||
elif self.resource_group:
|
||||
self.results['objects'] = self.list_resource_group()
|
||||
self.results['ansible_facts']['azure_publicipaddresses'] = self.list_resource_group()
|
||||
else:
|
||||
self.results['objects'] = self.list_all()
|
||||
self.results['ansible_facts']['azure_publicipaddresses'] = self.list_all()
|
||||
|
||||
return self.results
|
||||
|
||||
|
@ -157,7 +152,10 @@ class AzureRMPublicIPFacts(AzureRMModuleBase):
|
|||
pass
|
||||
|
||||
if item and self.has_tags(item.tags, self.tags):
|
||||
result = [self.serialize_obj(item, AZURE_OBJECT_CLASS)]
|
||||
pip = self.serialize_obj(item, AZURE_OBJECT_CLASS)
|
||||
pip['name'] = item.name
|
||||
pip['type'] = item.type
|
||||
result = [pip]
|
||||
|
||||
return result
|
||||
|
||||
|
@ -171,7 +169,10 @@ class AzureRMPublicIPFacts(AzureRMModuleBase):
|
|||
results = []
|
||||
for item in response:
|
||||
if self.has_tags(item.tags, self.tags):
|
||||
results.append(self.serialize_obj(item, AZURE_OBJECT_CLASS))
|
||||
pip = self.serialize_obj(item, AZURE_OBJECT_CLASS)
|
||||
pip['name'] = item.name
|
||||
pip['type'] = item.type
|
||||
results.append(pip)
|
||||
return results
|
||||
|
||||
def list_all(self):
|
||||
|
@ -184,7 +185,10 @@ class AzureRMPublicIPFacts(AzureRMModuleBase):
|
|||
results = []
|
||||
for item in response:
|
||||
if self.has_tags(item.tags, self.tags):
|
||||
results.append(self.serialize_obj(item, AZURE_OBJECT_CLASS))
|
||||
pip = self.serialize_obj(item, AZURE_OBJECT_CLASS)
|
||||
pip['name'] = item.name
|
||||
pip['type'] = item.type
|
||||
results.append(pip)
|
||||
return results
|
||||
|
||||
|
||||
|
|
|
@ -167,6 +167,8 @@ class AzureRMResourceGroup(AzureRMModuleBase):
|
|||
changed = True
|
||||
elif self.state == 'present':
|
||||
update_tags, results['tags'] = self.update_tags(results['tags'])
|
||||
self.log("update tags %s" % update_tags)
|
||||
self.log("new tags: %s" % str(results['tags']))
|
||||
if update_tags:
|
||||
changed = True
|
||||
|
||||
|
|
|
@ -66,16 +66,11 @@ EXAMPLES = '''
|
|||
- foo:bar
|
||||
'''
|
||||
RETURN = '''
|
||||
changed:
|
||||
description: Whether or not the object was changed.
|
||||
returned: always
|
||||
type: bool
|
||||
sample: False
|
||||
objects:
|
||||
description: List containing a set of facts for each selected object.
|
||||
azure_resourcegroups:
|
||||
description: List of resource group dicts.
|
||||
returned: always
|
||||
type: list
|
||||
sample: [{
|
||||
example: [{
|
||||
"id": "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing",
|
||||
"location": "westus",
|
||||
"name": "Testing",
|
||||
|
@ -114,7 +109,7 @@ class AzureRMResourceGroupFacts(AzureRMModuleBase):
|
|||
|
||||
self.results = dict(
|
||||
changed=False,
|
||||
results=[]
|
||||
ansible_facts=dict(azure_resourcegroups=[])
|
||||
)
|
||||
|
||||
self.name = None
|
||||
|
@ -130,9 +125,9 @@ class AzureRMResourceGroupFacts(AzureRMModuleBase):
|
|||
setattr(self, key, kwargs[key])
|
||||
|
||||
if self.name:
|
||||
self.results['objects'] = self.get_item()
|
||||
self.results['ansible_facts']['azure_resourcegroups'] = self.get_item()
|
||||
else:
|
||||
self.results['objects'] = self.list_items()
|
||||
self.results['ansible_facts']['azure_resourcegroups'] = self.list_items()
|
||||
|
||||
return self.results
|
||||
|
||||
|
|
|
@ -461,14 +461,14 @@ def create_rule_dict_from_obj(rule):
|
|||
id=rule.id,
|
||||
name=rule.name,
|
||||
description=rule.description,
|
||||
protocol=rule.protocol.value,
|
||||
protocol=rule.protocol,
|
||||
source_port_range=rule.source_port_range,
|
||||
destination_port_range=rule.destination_port_range,
|
||||
source_address_prefix=rule.source_address_prefix,
|
||||
destination_address_prefix=rule.destination_address_prefix,
|
||||
access=rule.access.value,
|
||||
access=rule.access,
|
||||
priority=rule.priority,
|
||||
direction=rule.direction.value,
|
||||
direction=rule.direction,
|
||||
provisioning_state=rule.provisioning_state,
|
||||
etag=rule.etag
|
||||
)
|
||||
|
|
|
@ -68,16 +68,11 @@ EXAMPLES = '''
|
|||
'''
|
||||
|
||||
RETURN = '''
|
||||
changed:
|
||||
description: Whether or not the object was changed.
|
||||
returned: always
|
||||
type: bool
|
||||
sample: False
|
||||
objects:
|
||||
description: List containing a set of facts for each selected object.
|
||||
azure_securitygroups:
|
||||
description: List containing security group dicts.
|
||||
returned: always
|
||||
type: list
|
||||
sample: [{
|
||||
example: [{
|
||||
"etag": 'W/"d036f4d7-d977-429a-a8c6-879bc2523399"',
|
||||
"id": "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Network/networkSecurityGroups/secgroup001",
|
||||
"location": "eastus2",
|
||||
|
@ -229,7 +224,7 @@ class AzureRMSecurityGroupFacts(AzureRMModuleBase):
|
|||
|
||||
self.results = dict(
|
||||
changed=False,
|
||||
objects=[]
|
||||
ansible_facts=dict(azure_securitygroups=[])
|
||||
)
|
||||
|
||||
self.name = None
|
||||
|
@ -245,9 +240,9 @@ class AzureRMSecurityGroupFacts(AzureRMModuleBase):
|
|||
setattr(self, key, kwargs[key])
|
||||
|
||||
if self.name is not None:
|
||||
self.results['objects'] = self.get_item()
|
||||
self.results['ansible_facts']['azure_securitygroups'] = self.get_item()
|
||||
else:
|
||||
self.results['objects'] = self.list_items()
|
||||
self.results['ansible_facts']['azure_securitygroups'] = self.list_items()
|
||||
|
||||
return self.results
|
||||
|
||||
|
@ -262,7 +257,9 @@ class AzureRMSecurityGroupFacts(AzureRMModuleBase):
|
|||
pass
|
||||
|
||||
if item and self.has_tags(item.tags, self.tags):
|
||||
result = [self.serialize_obj(item, AZURE_OBJECT_CLASS)]
|
||||
grp = self.serialize_obj(item, AZURE_OBJECT_CLASS)
|
||||
grp['name'] = item.name
|
||||
result = [grp]
|
||||
|
||||
return result
|
||||
|
||||
|
@ -276,7 +273,9 @@ class AzureRMSecurityGroupFacts(AzureRMModuleBase):
|
|||
results = []
|
||||
for item in response:
|
||||
if self.has_tags(item.tags, self.tags):
|
||||
results.append(self.serialize_obj(item, AZURE_OBJECT_CLASS))
|
||||
grp = self.serialize_obj(item, AZURE_OBJECT_CLASS)
|
||||
grp['name'] = item.name
|
||||
results.append(grp)
|
||||
return results
|
||||
|
||||
|
||||
|
|
|
@ -53,9 +53,9 @@ options:
|
|||
default: resource_group location
|
||||
account_type:
|
||||
description:
|
||||
- "Type of storage account. Required when creating a storage account. NOTE: StandardZRS and PremiumLRS
|
||||
- "Type of storage account. Required when creating a storage account. NOTE: Standard_ZRS and Premium_LRS
|
||||
accounts cannot be changed to other account types, and other account types cannot be changed to
|
||||
StandardZRS or PremiumLRS."
|
||||
Standard_ZRS or Premium_LRS."
|
||||
required: false
|
||||
default: null
|
||||
choices:
|
||||
|
@ -74,6 +74,15 @@ options:
|
|||
- Can be added to an existing storage account. Will be ignored during storage account creation.
|
||||
required: false
|
||||
default: null
|
||||
kind:
|
||||
description:
|
||||
- The 'kind' of storage.
|
||||
required: false
|
||||
default: 'Storage'
|
||||
choices:
|
||||
- Storage
|
||||
- StorageBlob
|
||||
version_added: "2.2"
|
||||
|
||||
extends_documentation_fragment:
|
||||
- azure
|
||||
|
@ -143,10 +152,9 @@ try:
|
|||
from msrestazure.azure_exceptions import CloudError
|
||||
from azure.storage.cloudstorageaccount import CloudStorageAccount
|
||||
from azure.common import AzureMissingResourceHttpError, AzureHttpError
|
||||
from azure.mgmt.storage.models import AccountType,\
|
||||
ProvisioningState, \
|
||||
StorageAccountUpdateParameters,\
|
||||
CustomDomain, StorageAccountCreateParameters
|
||||
from azure.mgmt.storage.models.storage_management_client_enums import ProvisioningState, SkuName, SkuTier, Kind
|
||||
from azure.mgmt.storage.models import StorageAccountUpdateParameters, CustomDomain, \
|
||||
StorageAccountCreateParameters, Sku
|
||||
except ImportError:
|
||||
# This is handled in azure_rm_common
|
||||
pass
|
||||
|
@ -168,9 +176,10 @@ class AzureRMStorageAccount(AzureRMModuleBase):
|
|||
state=dict(default='present', choices=['present', 'absent']),
|
||||
force=dict(type='bool', default=False),
|
||||
tags=dict(type='dict'),
|
||||
kind=dict(type='str', default='Storage', choices=['Storage', 'BlobStorage'])
|
||||
)
|
||||
|
||||
for key in AccountType:
|
||||
for key in SkuName:
|
||||
self.module_arg_spec['account_type']['choices'].append(getattr(key, 'value'))
|
||||
|
||||
self.results = dict(
|
||||
|
@ -187,6 +196,7 @@ class AzureRMStorageAccount(AzureRMModuleBase):
|
|||
self.custom_domain = None
|
||||
self.tags = None
|
||||
self.force = None
|
||||
self.kind = None
|
||||
|
||||
super(AzureRMStorageAccount, self).__init__(self.module_arg_spec,
|
||||
supports_check_mode=True)
|
||||
|
@ -270,7 +280,8 @@ class AzureRMStorageAccount(AzureRMModuleBase):
|
|||
location=account_obj.location,
|
||||
resource_group=self.resource_group,
|
||||
type=account_obj.type,
|
||||
account_type=account_obj.account_type.value,
|
||||
sku_tier=account_obj.sku.tier.value,
|
||||
sku_name=account_obj.sku.name.value,
|
||||
provisioning_state=account_obj.provisioning_state.value,
|
||||
secondary_location=account_obj.secondary_location,
|
||||
status_of_primary=(account_obj.status_of_primary.value
|
||||
|
@ -308,22 +319,26 @@ class AzureRMStorageAccount(AzureRMModuleBase):
|
|||
def update_account(self):
|
||||
self.log('Update storage account {0}'.format(self.name))
|
||||
if self.account_type:
|
||||
if self.account_type != self.account_dict['account_type']:
|
||||
if self.account_type != self.account_dict['sku_name']:
|
||||
# change the account type
|
||||
if self.account_dict['account_type'] in [AccountType.premium_lrs, AccountType.standard_zrs]:
|
||||
if self.account_dict['sku_name'] in [SkuName.premium_lrs, SkuName.standard_zrs]:
|
||||
self.fail("Storage accounts of type {0} and {1} cannot be changed.".format(
|
||||
AccountType.premium_lrs, AccountType.standard_zrs))
|
||||
if self.account_type in [AccountType.premium_lrs, AccountType.standard_zrs]:
|
||||
SkuName.premium_lrs, SkuName.standard_zrs))
|
||||
if self.account_type in [SkuName.premium_lrs, SkuName.standard_zrs]:
|
||||
self.fail("Storage account of type {0} cannot be changed to a type of {1} or {2}.".format(
|
||||
self.account_dict['account_type'], AccountType.premium_lrs, AccountType.standard_zrs))
|
||||
self.account_dict['sku_name'], SkuName.premium_lrs, SkuName.standard_zrs))
|
||||
|
||||
self.results['changed'] = True
|
||||
self.account_dict['account_type'] = self.account_type
|
||||
self.account_dict['sku_name'] = self.account_type
|
||||
|
||||
if self.results['changed'] and not self.check_mode:
|
||||
# Perform the update. The API only allows changing one attribute per call.
|
||||
try:
|
||||
parameters = StorageAccountUpdateParameters(account_type=self.account_dict['account_type'])
|
||||
self.log("sku_name: %s" % self.account_dict['sku_name'])
|
||||
self.log("sku_tier: %s" % self.account_dict['sku_tier'])
|
||||
sku = Sku(SkuName(self.account_dict['sku_name']))
|
||||
sku.tier = SkuTier(self.account_dict['sku_tier'])
|
||||
parameters = StorageAccountUpdateParameters(sku=sku)
|
||||
self.storage_client.storage_accounts.update(self.resource_group,
|
||||
self.name,
|
||||
parameters)
|
||||
|
@ -378,8 +393,9 @@ class AzureRMStorageAccount(AzureRMModuleBase):
|
|||
if self.tags:
|
||||
account_dict['tags'] = self.tags
|
||||
return account_dict
|
||||
parameters = StorageAccountCreateParameters(account_type=self.account_type, location=self.location,
|
||||
tags=self.tags)
|
||||
sku = Sku(SkuName(self.account_type))
|
||||
sku.tier = SkuTier.standard if 'Standard' in self.account_type else SkuTier['Pemium']
|
||||
parameters = StorageAccountCreateParameters(sku, self.kind, self.location, tags=self.tags)
|
||||
self.log(str(parameters))
|
||||
try:
|
||||
poller = self.storage_client.storage_accounts.create(self.resource_group, self.name, parameters)
|
||||
|
@ -412,22 +428,9 @@ class AzureRMStorageAccount(AzureRMModuleBase):
|
|||
not be deleted.
|
||||
'''
|
||||
self.log('Checking for existing blob containers')
|
||||
keys = dict()
|
||||
blob_service = self.get_blob_client(self.resource_group, self.name)
|
||||
try:
|
||||
# Get keys from the storage account
|
||||
account_keys = self.storage_client.storage_accounts.list_keys(self.resource_group, self.name)
|
||||
keys['key1'] = account_keys.key1
|
||||
keys['key2'] = account_keys.key2
|
||||
except AzureHttpError as e:
|
||||
self.fail("check_for_container:Failed to get account keys: {0}".format(e))
|
||||
|
||||
try:
|
||||
cloud_storage = CloudStorageAccount(self.name, keys['key1']).create_page_blob_service()
|
||||
except Exception as e:
|
||||
self.fail("check_for_container:Error creating blob service: {0}".format(e))
|
||||
|
||||
try:
|
||||
response = cloud_storage.list_containers()
|
||||
response = blob_service.list_containers()
|
||||
except AzureMissingResourceHttpError:
|
||||
# No blob storage available?
|
||||
return False
|
||||
|
|
|
@ -74,16 +74,11 @@ EXAMPLES = '''
|
|||
'''
|
||||
|
||||
RETURN = '''
|
||||
changed:
|
||||
description: Whether or not the object was changed.
|
||||
returned: always
|
||||
type: bool
|
||||
sample: False
|
||||
objects:
|
||||
description: List containing a set of facts for each selected object.
|
||||
azure_storageaccounts:
|
||||
description: List of storage account dicts.
|
||||
returned: always
|
||||
type: list
|
||||
sample: [{
|
||||
example: [{
|
||||
"id": "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/testing/providers/Microsoft.Storage/storageAccounts/testaccount001",
|
||||
"location": "eastus2",
|
||||
"name": "testaccount001",
|
||||
|
@ -130,7 +125,7 @@ class AzureRMStorageAccountFacts(AzureRMModuleBase):
|
|||
|
||||
self.results = dict(
|
||||
changed=False,
|
||||
objects=[]
|
||||
ansible_facts=dict(azure_storageaccounts=[])
|
||||
)
|
||||
|
||||
self.name = None
|
||||
|
@ -150,11 +145,11 @@ class AzureRMStorageAccountFacts(AzureRMModuleBase):
|
|||
self.fail("Parameter error: resource group required when filtering by name.")
|
||||
|
||||
if self.name:
|
||||
self.results['objects'] = self.get_account()
|
||||
self.results['ansible_facts']['azure_storageaccounts'] = self.get_account()
|
||||
elif self.resource_group:
|
||||
self.results['objects'] = self.list_resource_group()
|
||||
self.results['ansible_facts']['azure_storageaccounts'] = self.list_resource_group()
|
||||
else:
|
||||
self.results['objects'] = self.list_all()
|
||||
self.results['ansible_facts']['azure_storageaccounts'] = self.list_all()
|
||||
|
||||
return self.results
|
||||
|
||||
|
|
|
@ -95,18 +95,36 @@ EXAMPLES = '''
|
|||
RETURN = '''
|
||||
state:
|
||||
description: Current state of the subnet.
|
||||
returned: always
|
||||
type: dict
|
||||
sample: {
|
||||
"address_prefix": "10.1.0.0/16",
|
||||
"id": "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Network/virtualNetworks/My_Virtual_Network/subnets/foobar",
|
||||
"name": "foobar",
|
||||
"network_security_group": {
|
||||
"id": "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Network/networkSecurityGroups/secgroupfoo",
|
||||
"name": "secgroupfoo"
|
||||
},
|
||||
"provisioning_state": "Succeeded"
|
||||
}
|
||||
returned: success
|
||||
type: complex
|
||||
contains:
|
||||
address_prefix:
|
||||
description: IP address CIDR.
|
||||
type: str
|
||||
example: "10.1.0.0/16"
|
||||
id:
|
||||
description: Subnet resource path.
|
||||
type: str
|
||||
example: "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Network/virtualNetworks/My_Virtual_Network/subnets/foobar"
|
||||
name:
|
||||
description: Subnet name.
|
||||
type: str
|
||||
example: "foobar"
|
||||
network_security_group:
|
||||
type: complex
|
||||
contains:
|
||||
id:
|
||||
description: Security group resource identifier.
|
||||
type: str
|
||||
example: "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Network/networkSecurityGroups/secgroupfoo"
|
||||
name:
|
||||
description: Name of the security group.
|
||||
type: str
|
||||
example: "secgroupfoo"
|
||||
provisioning_state:
|
||||
description: Success or failure of the provisioning event.
|
||||
type: str
|
||||
example: "Succeeded"
|
||||
'''
|
||||
|
||||
|
||||
|
@ -235,7 +253,6 @@ class AzureRMSubnet(AzureRMModuleBase):
|
|||
)
|
||||
if nsg:
|
||||
subnet.network_security_group = NetworkSecurityGroup(id=nsg.id,
|
||||
name=nsg.name,
|
||||
location=nsg.location,
|
||||
resource_guid=nsg.resource_guid)
|
||||
|
||||
|
@ -248,7 +265,6 @@ class AzureRMSubnet(AzureRMModuleBase):
|
|||
if results['network_security_group'].get('id'):
|
||||
nsg = self.get_security_group(results['network_security_group']['name'])
|
||||
subnet.network_security_group = NetworkSecurityGroup(id=nsg.id,
|
||||
name=nsg.name,
|
||||
location=nsg.location,
|
||||
resource_guid=nsg.resource_guid)
|
||||
|
||||
|
@ -280,7 +296,7 @@ class AzureRMSubnet(AzureRMModuleBase):
|
|||
poller = self.network_client.subnets.delete(self.resource_group,
|
||||
self.virtual_network_name,
|
||||
self.name)
|
||||
result = self.get_poller_results(poller)
|
||||
result = self.get_poller_result(poller)
|
||||
except Exception as exc:
|
||||
self.fail("Error deleting subnet {0} - {1}".format(self.name, str(exc)))
|
||||
|
||||
|
|
|
@ -288,12 +288,27 @@ powerstate:
|
|||
description: Indicates if the state is running, stopped, deallocated
|
||||
returned: always
|
||||
type: string
|
||||
sample: running
|
||||
azure_rm_vm:
|
||||
example: running
|
||||
deleted_vhd_uris:
|
||||
description: List of deleted Virtual Hard Disk URIs.
|
||||
returned: 'on delete'
|
||||
type: list
|
||||
example: ["https://testvm104519.blob.core.windows.net/vhds/testvm10.vhd"]
|
||||
deleted_network_interfaces:
|
||||
description: List of deleted NICs.
|
||||
returned: 'on delete'
|
||||
type: list
|
||||
example: ["testvm1001"]
|
||||
deleted_public_ips:
|
||||
description: List of deleted publid IP addrees names.
|
||||
returned: 'on delete'
|
||||
type: list
|
||||
example: ["testvm1001"]
|
||||
azure_vm:
|
||||
description: Facts about the current state of the object. Note that facts are not part of the registered output but available directly.
|
||||
returned: always
|
||||
type: dict
|
||||
sample: {
|
||||
type: complex
|
||||
example: {
|
||||
"properties": {
|
||||
"hardwareProfile": {
|
||||
"vmSize": "Standard_D1"
|
||||
|
@ -426,19 +441,25 @@ from ansible.module_utils.azure_rm_common import *
|
|||
|
||||
try:
|
||||
from msrestazure.azure_exceptions import CloudError
|
||||
from azure.mgmt.compute.models import NetworkInterfaceReference, VirtualMachine, HardwareProfile, \
|
||||
StorageProfile, OSProfile, OSDisk, VirtualHardDisk, ImageReference, NetworkProfile, LinuxConfiguration, \
|
||||
SshConfiguration, SshPublicKey
|
||||
from azure.mgmt.compute.models import NetworkInterfaceReference, \
|
||||
VirtualMachine, HardwareProfile, \
|
||||
StorageProfile, OSProfile, OSDisk, \
|
||||
VirtualHardDisk, ImageReference,\
|
||||
NetworkProfile, LinuxConfiguration, \
|
||||
SshConfiguration, SshPublicKey
|
||||
from azure.mgmt.network.models import PublicIPAddress, NetworkSecurityGroup, NetworkInterface, \
|
||||
NetworkInterfaceIPConfiguration, Subnet
|
||||
from azure.mgmt.storage.models import StorageAccountCreateParameters
|
||||
from azure.mgmt.compute.models.compute_management_client_enums import DiskCreateOptionTypes, VirtualMachineSizeTypes
|
||||
NetworkInterfaceIPConfiguration, Subnet
|
||||
from azure.mgmt.storage.models import StorageAccountCreateParameters, Sku
|
||||
from azure.mgmt.storage.models.storage_management_client_enums import Kind, SkuTier, SkuName
|
||||
from azure.mgmt.compute.models.compute_management_client_enums import VirtualMachineSizeTypes, DiskCreateOptionTypes
|
||||
except ImportError:
|
||||
# This is handled in azure_rm_common
|
||||
pass
|
||||
|
||||
AZURE_OBJECT_CLASS = 'VirtualMachine'
|
||||
|
||||
AZURE_ENUM_MODULES = ['azure.mgmt.compute.models.compute_management_client_enums']
|
||||
|
||||
|
||||
def extract_names_from_blob_uri(blob_uri):
|
||||
# HACK: ditch this once python SDK supports get by URI
|
||||
|
@ -520,20 +541,20 @@ class AzureRMVirtualMachine(AzureRMModuleBase):
|
|||
changed=False,
|
||||
actions=[],
|
||||
powerstate_change=None,
|
||||
ansible_facts=dict(azure_rm_vm=None)
|
||||
ansible_facts=dict(azure_vm=None)
|
||||
)
|
||||
|
||||
super(AzureRMVirtualMachine, self).__init__(derived_arg_spec=self.module_arg_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
# make sure options are lower case
|
||||
self.remove_on_absent = set([resource.lower() for resource in self.remove_on_absent])
|
||||
|
||||
def exec_module(self, **kwargs):
|
||||
|
||||
for key in self.module_arg_spec.keys() + ['tags']:
|
||||
setattr(self, key, kwargs[key])
|
||||
|
||||
# make sure options are lower case
|
||||
self.remove_on_absent = set([resource.lower() for resource in self.remove_on_absent])
|
||||
|
||||
changed = False
|
||||
powerstate_change = None
|
||||
results = dict()
|
||||
|
@ -666,7 +687,7 @@ class AzureRMVirtualMachine(AzureRMModuleBase):
|
|||
changed = True
|
||||
|
||||
self.results['changed'] = changed
|
||||
self.results['ansible_facts']['azure_rm_vm'] = results
|
||||
self.results['ansible_facts']['azure_vm'] = results
|
||||
self.results['powerstate_change'] = powerstate_change
|
||||
|
||||
if self.check_mode:
|
||||
|
@ -712,8 +733,7 @@ class AzureRMVirtualMachine(AzureRMModuleBase):
|
|||
nics = [NetworkInterfaceReference(id=id) for id in network_interfaces]
|
||||
vhd = VirtualHardDisk(uri=requested_vhd_uri)
|
||||
vm_resource = VirtualMachine(
|
||||
location=self.location,
|
||||
name=self.name,
|
||||
self.location,
|
||||
tags=self.tags,
|
||||
os_profile=OSProfile(
|
||||
admin_username=self.admin_username,
|
||||
|
@ -755,7 +775,6 @@ class AzureRMVirtualMachine(AzureRMModuleBase):
|
|||
vm_resource.os_profile.linux_configuration.ssh = ssh_config
|
||||
|
||||
self.log("Create virtual machine with parameters:")
|
||||
self.log(self.serialize_obj(vm_resource, 'VirtualMachine'), pretty_print=True)
|
||||
self.create_or_update_vm(vm_resource)
|
||||
|
||||
elif self.differences and len(self.differences) > 0:
|
||||
|
@ -768,10 +787,8 @@ class AzureRMVirtualMachine(AzureRMModuleBase):
|
|||
for interface in vm_dict['properties']['networkProfile']['networkInterfaces']]
|
||||
vhd = VirtualHardDisk(uri=vm_dict['properties']['storageProfile']['osDisk']['vhd']['uri'])
|
||||
vm_resource = VirtualMachine(
|
||||
id=vm_dict['id'],
|
||||
location=vm_dict['location'],
|
||||
name=vm_dict['name'],
|
||||
type=vm_dict['type'],
|
||||
vm_dict['location'],
|
||||
vm_id=vm_dict['properties']['vmId'],
|
||||
os_profile=OSProfile(
|
||||
admin_username=vm_dict['properties']['osProfile']['adminUsername'],
|
||||
computer_name=vm_dict['properties']['osProfile']['computerName']
|
||||
|
@ -819,21 +836,19 @@ class AzureRMVirtualMachine(AzureRMModuleBase):
|
|||
vm_resource.os_profile.linux_configuration.ssh = SshConfiguration(public_keys=[])
|
||||
for key in public_keys:
|
||||
vm_resource.os_profile.linux_configuration.ssh.public_keys.append(
|
||||
SshConfiguration(
|
||||
path=key['path'],
|
||||
key_data=key['keyData']
|
||||
)
|
||||
SshPublicKey(path=key['path'], key_data=key['keyData'])
|
||||
)
|
||||
self.log("Update virtual machine with parameters:")
|
||||
self.log(self.serialize_obj(vm_resource, 'VirtualMachine'), pretty_print=True)
|
||||
self.create_or_update_vm(vm_resource)
|
||||
|
||||
# Make sure we leave the machine in requested power state
|
||||
if powerstate_change == 'poweron' and self.results['state']['powerstate'] != 'running':
|
||||
if powerstate_change == 'poweron' and \
|
||||
self.results['ansible_facts']['azure_vm']['powerstate'] != 'running':
|
||||
# Attempt to power on the machine
|
||||
self.power_on_vm()
|
||||
|
||||
elif powerstate_change == 'poweroff' and self.results['state']['powerstate'] == 'running':
|
||||
elif powerstate_change == 'poweroff' and \
|
||||
self.results['ansible_facts']['azure_vm']['powerstate'] == 'running':
|
||||
# Attempt to power off the machine
|
||||
self.power_off_vm()
|
||||
|
||||
|
@ -843,12 +858,12 @@ class AzureRMVirtualMachine(AzureRMModuleBase):
|
|||
elif powerstate_change == 'deallocated':
|
||||
self.deallocate_vm()
|
||||
|
||||
self.results['ansible_facts']['azure_rm_vm'] = self.serialize_vm(self.get_vm())
|
||||
self.results['ansible_facts']['azure_vm'] = self.serialize_vm(self.get_vm())
|
||||
|
||||
elif self.state == 'absent':
|
||||
# delete the VM
|
||||
self.log("Delete virtual machine {0}".format(self.name))
|
||||
self.results['ansible_facts']['azure_rm_vm'] = None
|
||||
self.results['ansible_facts']['azure_vm'] = None
|
||||
self.delete_vm(vm)
|
||||
|
||||
# until we sort out how we want to do this globally
|
||||
|
@ -875,9 +890,18 @@ class AzureRMVirtualMachine(AzureRMModuleBase):
|
|||
:param vm: VirtualMachine object
|
||||
:return: dict
|
||||
'''
|
||||
result = self.serialize_obj(vm, AZURE_OBJECT_CLASS)
|
||||
result['powerstate'] = next((s.code.replace('PowerState/', '')
|
||||
for s in vm.instance_view.statuses if s.code.startswith('PowerState')), None)
|
||||
|
||||
result = self.serialize_obj(vm, AZURE_OBJECT_CLASS, enum_modules=AZURE_ENUM_MODULES)
|
||||
result['id'] = vm.id
|
||||
result['name'] = vm.name
|
||||
result['type'] = vm.type
|
||||
result['location'] = vm.location
|
||||
result['tags'] = vm.tags
|
||||
|
||||
result['powerstate'] = dict()
|
||||
if vm.instance_view:
|
||||
result['powerstate'] = next((s.code.replace('PowerState/', '')
|
||||
for s in vm.instance_view.statuses if s.code.startswith('PowerState')), None)
|
||||
|
||||
# Expand network interfaces to include config properties
|
||||
for interface in vm.network_profile.network_interfaces:
|
||||
|
@ -1138,8 +1162,10 @@ class AzureRMVirtualMachine(AzureRMModuleBase):
|
|||
self.log("Storage account {0} found.".format(storage_account_name))
|
||||
self.check_provisioning_state(account)
|
||||
return account
|
||||
|
||||
parameters = StorageAccountCreateParameters(account_type='Standard_LRS', location=self.location)
|
||||
sku = Sku(SkuName.standard_lrs)
|
||||
Sku.tier = SkuTier.standard
|
||||
kind = Kind.storage
|
||||
parameters = StorageAccountCreateParameters(sku, kind, self.location)
|
||||
self.log("Creating storage account {0} in location {1}".format(storage_account_name, self.location))
|
||||
self.results['actions'].append("Created storage account {0}".format(storage_account_name))
|
||||
try:
|
||||
|
@ -1243,21 +1269,18 @@ class AzureRMVirtualMachine(AzureRMModuleBase):
|
|||
|
||||
parameters = NetworkInterface(
|
||||
location=self.location,
|
||||
name=network_interface_name,
|
||||
ip_configurations=[
|
||||
NetworkInterfaceIPConfiguration(
|
||||
name='default',
|
||||
private_ip_allocation_method='Dynamic',
|
||||
)
|
||||
]
|
||||
)
|
||||
parameters.ip_configurations[0].subnet = Subnet(id=subnet_id)
|
||||
parameters.ip_configurations[0].name = 'default'
|
||||
parameters.network_security_group = NetworkSecurityGroup(id=group.id,
|
||||
name=group.name,
|
||||
location=group.location,
|
||||
resource_guid=group.resource_guid)
|
||||
parameters.ip_configurations[0].public_ip_address = PublicIPAddress(id=pip.id,
|
||||
name=pip.name,
|
||||
location=pip.location,
|
||||
resource_guid=pip.resource_guid)
|
||||
|
||||
|
|
|
@ -100,16 +100,11 @@ EXAMPLES = '''
|
|||
'''
|
||||
|
||||
RETURN = '''
|
||||
changed:
|
||||
description: Whether or not the object was changed.
|
||||
returned: always
|
||||
type: bool
|
||||
sample: False
|
||||
objects:
|
||||
description: List containing a set of facts for each selected object.
|
||||
azure_vmimages:
|
||||
description: List of image dicts.
|
||||
returned: always
|
||||
type: list
|
||||
sample: []
|
||||
example: []
|
||||
'''
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
|
@ -122,6 +117,7 @@ except:
|
|||
# This is handled in azure_rm_common
|
||||
pass
|
||||
|
||||
AZURE_ENUM_MODULES = ['azure.mgmt.compute.models.compute_management_client_enums']
|
||||
|
||||
class AzureRMVirtualMachineImageFacts(AzureRMModuleBase):
|
||||
|
||||
|
@ -137,7 +133,7 @@ class AzureRMVirtualMachineImageFacts(AzureRMModuleBase):
|
|||
|
||||
self.results = dict(
|
||||
changed=False,
|
||||
results=[]
|
||||
ansible_facts=dict(azure_vmimages=[])
|
||||
)
|
||||
|
||||
self.location = None
|
||||
|
@ -154,13 +150,13 @@ class AzureRMVirtualMachineImageFacts(AzureRMModuleBase):
|
|||
setattr(self, key, kwargs[key])
|
||||
|
||||
if self.location and self.publisher and self.offer and self.sku and self.version:
|
||||
self.results['objects'] = self.get_item()
|
||||
self.results['ansible_facts']['azure_vmimages'] = self.get_item()
|
||||
elif self.location and self.publisher and self.offer and self.sku:
|
||||
self.results['objects'] = self.list_images()
|
||||
self.results['ansible_facts']['azure_vmimages'] = self.list_images()
|
||||
elif self.location and self.publisher:
|
||||
self.results['objects'] = self.list_offers()
|
||||
self.results['ansible_facts']['azure_vmimages'] = self.list_offers()
|
||||
elif self.location:
|
||||
self.results['objects'] = self.list_publishers()
|
||||
self.results['ansible_facts']['azure_vmimages'] = self.list_publishers()
|
||||
|
||||
return self.results
|
||||
|
||||
|
@ -178,7 +174,7 @@ class AzureRMVirtualMachineImageFacts(AzureRMModuleBase):
|
|||
pass
|
||||
|
||||
if item:
|
||||
result = [self.serialize_obj(item, 'VirtualMachineImage')]
|
||||
result = [self.serialize_obj(item, 'VirtualMachineImage', enum_modules=AZURE_ENUM_MODULES)]
|
||||
|
||||
return result
|
||||
|
||||
|
@ -197,7 +193,8 @@ class AzureRMVirtualMachineImageFacts(AzureRMModuleBase):
|
|||
|
||||
if response:
|
||||
for item in response:
|
||||
results.append(self.serialize_obj(item, 'VirtualMachineImageResource'))
|
||||
results.append(self.serialize_obj(item, 'VirtualMachineImageResource',
|
||||
enum_modules=AZURE_ENUM_MODULES))
|
||||
return results
|
||||
|
||||
def list_offers(self):
|
||||
|
@ -213,7 +210,8 @@ class AzureRMVirtualMachineImageFacts(AzureRMModuleBase):
|
|||
|
||||
if response:
|
||||
for item in response:
|
||||
results.append(self.serialize_obj(item, 'VirtualMachineImageResource'))
|
||||
results.append(self.serialize_obj(item, 'VirtualMachineImageResource',
|
||||
enum_modules=AZURE_ENUM_MODULES))
|
||||
return results
|
||||
|
||||
def list_publishers(self):
|
||||
|
@ -228,7 +226,8 @@ class AzureRMVirtualMachineImageFacts(AzureRMModuleBase):
|
|||
|
||||
if response:
|
||||
for item in response:
|
||||
results.append(self.serialize_obj(item, 'VirtualMachineImageResource'))
|
||||
results.append(self.serialize_obj(item, 'VirtualMachineImageResource',
|
||||
enum_modules=AZURE_ENUM_MODULES))
|
||||
return results
|
||||
|
||||
|
||||
|
|
|
@ -72,16 +72,11 @@ EXAMPLES = '''
|
|||
- testing
|
||||
'''
|
||||
RETURN = '''
|
||||
changed:
|
||||
description: Whether or not the object was changed.
|
||||
returned: always
|
||||
type: bool
|
||||
sample: False
|
||||
objects:
|
||||
description: List containing a set of facts for each selected object.
|
||||
azure_virtualnetworks:
|
||||
description: List of virtual network dicts.
|
||||
returned: always
|
||||
type: list
|
||||
sample: [{
|
||||
example: [{
|
||||
"etag": 'W/"532ba1be-ae71-40f2-9232-3b1d9cf5e37e"',
|
||||
"id": "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/Testing/providers/Microsoft.Network/virtualNetworks/vnet2001",
|
||||
"location": "eastus2",
|
||||
|
@ -126,7 +121,7 @@ class AzureRMNetworkInterfaceFacts(AzureRMModuleBase):
|
|||
|
||||
self.results = dict(
|
||||
changed=False,
|
||||
objects=[]
|
||||
ansible_facts=dict(azure_virtualnetworks=[])
|
||||
)
|
||||
|
||||
self.name = None
|
||||
|
@ -143,9 +138,9 @@ class AzureRMNetworkInterfaceFacts(AzureRMModuleBase):
|
|||
setattr(self, key, kwargs[key])
|
||||
|
||||
if self.name is not None:
|
||||
self.results['objects'] = self.get_item()
|
||||
self.results['ansible_facts']['azure_virtualnetworks'] = self.get_item()
|
||||
else:
|
||||
self.results['objects'] = self.list_items()
|
||||
self.results['ansible_facts']['azure_virtualnetworks'] = self.list_items()
|
||||
|
||||
return self.results
|
||||
|
||||
|
|
Loading…
Reference in a new issue