salt/states/azurearm_resource.py
# -*- coding: utf-8 -*-
'''
Azure (ARM) Resource State Module
.. versionadded:: 2019.2.0
:maintainer: <devops@decisionlab.io>
:maturity: new
:depends:
* `azure <https://pypi.python.org/pypi/azure>`_ >= 2.0.0
* `azure-common <https://pypi.python.org/pypi/azure-common>`_ >= 1.1.8
* `azure-mgmt <https://pypi.python.org/pypi/azure-mgmt>`_ >= 1.0.0
* `azure-mgmt-compute <https://pypi.python.org/pypi/azure-mgmt-compute>`_ >= 1.0.0
* `azure-mgmt-network <https://pypi.python.org/pypi/azure-mgmt-network>`_ >= 1.7.1
* `azure-mgmt-resource <https://pypi.python.org/pypi/azure-mgmt-resource>`_ >= 1.1.0
* `azure-mgmt-storage <https://pypi.python.org/pypi/azure-mgmt-storage>`_ >= 1.0.0
* `azure-mgmt-web <https://pypi.python.org/pypi/azure-mgmt-web>`_ >= 0.32.0
* `azure-storage <https://pypi.python.org/pypi/azure-storage>`_ >= 0.34.3
* `msrestazure <https://pypi.python.org/pypi/msrestazure>`_ >= 0.4.21
:platform: linux
:configuration: This module requires Azure Resource Manager credentials to be passed as a dictionary of
keyword arguments to the ``connection_auth`` parameter in order to work properly. Since the authentication
parameters are sensitive, it's recommended to pass them to the states via pillar.
Required provider parameters:
if using username and password:
* ``subscription_id``
* ``username``
* ``password``
if using a service principal:
* ``subscription_id``
* ``tenant``
* ``client_id``
* ``secret``
Optional provider parameters:
**cloud_environment**: Used to point the cloud driver to different API endpoints, such as Azure GovCloud. Possible values:
* ``AZURE_PUBLIC_CLOUD`` (default)
* ``AZURE_CHINA_CLOUD``
* ``AZURE_US_GOV_CLOUD``
* ``AZURE_GERMAN_CLOUD``
Example Pillar for Azure Resource Manager authentication:
.. code-block:: yaml
azurearm:
user_pass_auth:
subscription_id: 3287abc8-f98a-c678-3bde-326766fd3617
username: fletch
password: 123pass
mysubscription:
subscription_id: 3287abc8-f98a-c678-3bde-326766fd3617
tenant: ABCDEFAB-1234-ABCD-1234-ABCDEFABCDEF
client_id: ABCDEFAB-1234-ABCD-1234-ABCDEFABCDEF
secret: XXXXXXXXXXXXXXXXXXXXXXXX
cloud_environment: AZURE_PUBLIC_CLOUD
Example states using Azure Resource Manager authentication:
.. code-block:: jinja
{% set profile = salt['pillar.get']('azurearm:mysubscription') %}
Ensure resource group exists:
azurearm_resource.resource_group_present:
- name: my_rg
- location: westus
- tags:
how_awesome: very
contact_name: Elmer Fudd Gantry
- connection_auth: {{ profile }}
Ensure resource group is absent:
azurearm_resource.resource_group_absent:
- name: other_rg
- connection_auth: {{ profile }}
'''
# Import Python libs
from __future__ import absolute_import
import json
import logging
# Import Salt libs
import salt.utils.files
__virtualname__ = 'azurearm_resource'
log = logging.getLogger(__name__)
def __virtual__():
'''
Only make this state available if the azurearm_resource module is available.
'''
return __virtualname__ if 'azurearm_resource.resource_group_check_existence' in __salt__ else False
def resource_group_present(name, location, managed_by=None, tags=None, connection_auth=None, **kwargs):
'''
.. versionadded:: 2019.2.0
Ensure a resource group exists.
:param name:
Name of the resource group.
:param location:
The Azure location in which to create the resource group. This value cannot be updated once
the resource group is created.
:param managed_by:
The ID of the resource that manages this resource group. This value cannot be updated once
the resource group is created.
:param tags:
A dictionary of strings can be passed as tag metadata to the resource group object.
:param connection_auth:
A dict with subscription and authentication parameters to be used in connecting to the
Azure Resource Manager API.
Example usage:
.. code-block:: yaml
Ensure resource group exists:
azurearm_resource.resource_group_present:
- name: group1
- location: eastus
- tags:
contact_name: Elmer Fudd Gantry
- connection_auth: {{ profile }}
'''
ret = {
'name': name,
'result': False,
'comment': '',
'changes': {}
}
if not isinstance(connection_auth, dict):
ret['comment'] = 'Connection information must be specified via connection_auth dictionary!'
return ret
group = {}
present = __salt__['azurearm_resource.resource_group_check_existence'](name, **connection_auth)
if present:
group = __salt__['azurearm_resource.resource_group_get'](name, **connection_auth)
ret['changes'] = __utils__['dictdiffer.deep_diff'](group.get('tags', {}), tags or {})
if not ret['changes']:
ret['result'] = True
ret['comment'] = 'Resource group {0} is already present.'.format(name)
return ret
if __opts__['test']:
ret['comment'] = 'Resource group {0} tags would be updated.'.format(name)
ret['result'] = None
ret['changes'] = {
'old': group.get('tags', {}),
'new': tags
}
return ret
elif __opts__['test']:
ret['comment'] = 'Resource group {0} would be created.'.format(name)
ret['result'] = None
ret['changes'] = {
'old': {},
'new': {
'name': name,
'location': location,
'managed_by': managed_by,
'tags': tags,
}
}
return ret
group_kwargs = kwargs.copy()
group_kwargs.update(connection_auth)
group = __salt__['azurearm_resource.resource_group_create_or_update'](
name,
location,
managed_by=managed_by,
tags=tags,
**group_kwargs
)
present = __salt__['azurearm_resource.resource_group_check_existence'](name, **connection_auth)
if present:
ret['result'] = True
ret['comment'] = 'Resource group {0} has been created.'.format(name)
ret['changes'] = {
'old': {},
'new': group
}
return ret
ret['comment'] = 'Failed to create resource group {0}! ({1})'.format(name, group.get('error'))
return ret
def resource_group_absent(name, connection_auth=None):
'''
.. versionadded:: 2019.2.0
Ensure a resource group does not exist in the current subscription.
:param name:
Name of the resource group.
:param connection_auth:
A dict with subscription and authentication parameters to be used in connecting to the
Azure Resource Manager API.
'''
ret = {
'name': name,
'result': False,
'comment': '',
'changes': {}
}
if not isinstance(connection_auth, dict):
ret['comment'] = 'Connection information must be specified via connection_auth dictionary!'
return ret
group = {}
present = __salt__['azurearm_resource.resource_group_check_existence'](name, **connection_auth)
if not present:
ret['result'] = True
ret['comment'] = 'Resource group {0} is already absent.'.format(name)
return ret
elif __opts__['test']:
group = __salt__['azurearm_resource.resource_group_get'](name, **connection_auth)
ret['comment'] = 'Resource group {0} would be deleted.'.format(name)
ret['result'] = None
ret['changes'] = {
'old': group,
'new': {},
}
return ret
group = __salt__['azurearm_resource.resource_group_get'](name, **connection_auth)
deleted = __salt__['azurearm_resource.resource_group_delete'](name, **connection_auth)
if deleted:
present = False
else:
present = __salt__['azurearm_resource.resource_group_check_existence'](name, **connection_auth)
if not present:
ret['result'] = True
ret['comment'] = 'Resource group {0} has been deleted.'.format(name)
ret['changes'] = {
'old': group,
'new': {}
}
return ret
ret['comment'] = 'Failed to delete resource group {0}!'.format(name)
return ret
def policy_definition_present(name, policy_rule=None, policy_type=None, mode=None, display_name=None, description=None,
metadata=None, parameters=None, policy_rule_json=None, policy_rule_file=None,
template='jinja', source_hash=None, source_hash_name=None, skip_verify=False,
connection_auth=None, **kwargs):
'''
.. versionadded:: 2019.2.0
Ensure a security policy definition exists.
:param name:
Name of the policy definition.
:param policy_rule:
A YAML dictionary defining the policy rule. See `Azure Policy Definition documentation
<https://docs.microsoft.com/en-us/azure/azure-policy/policy-definition#policy-rule>`_ for details on the
structure. One of ``policy_rule``, ``policy_rule_json``, or ``policy_rule_file`` is required, in that order of
precedence for use if multiple parameters are used.
:param policy_rule_json:
A text field defining the entirety of a policy definition in JSON. See `Azure Policy Definition documentation
<https://docs.microsoft.com/en-us/azure/azure-policy/policy-definition#policy-rule>`_ for details on the
structure. One of ``policy_rule``, ``policy_rule_json``, or ``policy_rule_file`` is required, in that order of
precedence for use if multiple parameters are used. Note that the `name` field in the JSON will override the
``name`` parameter in the state.
:param policy_rule_file:
The source of a JSON file defining the entirety of a policy definition. See `Azure Policy Definition
documentation <https://docs.microsoft.com/en-us/azure/azure-policy/policy-definition#policy-rule>`_ for
details on the structure. One of ``policy_rule``, ``policy_rule_json``, or ``policy_rule_file`` is required,
in that order of precedence for use if multiple parameters are used. Note that the `name` field in the JSON
will override the ``name`` parameter in the state.
:param skip_verify:
Used for the ``policy_rule_file`` parameter. If ``True``, hash verification of remote file sources
(``http://``, ``https://``, ``ftp://``) will be skipped, and the ``source_hash`` argument will be ignored.
:param source_hash:
This can be a source hash string or the URI of a file that contains source hash strings.
:param source_hash_name:
When ``source_hash`` refers to a hash file, Salt will try to find the correct hash by matching the
filename/URI associated with that hash.
:param policy_type:
The type of policy definition. Possible values are NotSpecified, BuiltIn, and Custom. Only used with the
``policy_rule`` parameter.
:param mode:
The policy definition mode. Possible values are NotSpecified, Indexed, and All. Only used with the
``policy_rule`` parameter.
:param display_name:
The display name of the policy definition. Only used with the ``policy_rule`` parameter.
:param description:
The policy definition description. Only used with the ``policy_rule`` parameter.
:param metadata:
The policy definition metadata defined as a dictionary. Only used with the ``policy_rule`` parameter.
:param parameters:
Required dictionary if a parameter is used in the policy rule. Only used with the ``policy_rule`` parameter.
:param connection_auth:
A dict with subscription and authentication parameters to be used in connecting to the
Azure Resource Manager API.
Example usage:
.. code-block:: yaml
Ensure policy definition exists:
azurearm_resource.policy_definition_present:
- name: testpolicy
- display_name: Test Policy
- description: Test policy for testing policies.
- policy_rule:
if:
allOf:
- equals: Microsoft.Compute/virtualMachines/write
source: action
- field: location
in:
- eastus
- eastus2
- centralus
then:
effect: deny
- connection_auth: {{ profile }}
'''
ret = {
'name': name,
'result': False,
'comment': '',
'changes': {}
}
if not isinstance(connection_auth, dict):
ret['comment'] = 'Connection information must be specified via connection_auth dictionary!'
return ret
if not policy_rule and not policy_rule_json and not policy_rule_file:
ret['comment'] = 'One of "policy_rule", "policy_rule_json", or "policy_rule_file" is required!'
return ret
if sum(x is not None for x in [policy_rule, policy_rule_json, policy_rule_file]) > 1:
ret['comment'] = 'Only one of "policy_rule", "policy_rule_json", or "policy_rule_file" is allowed!'
return ret
if ((policy_rule_json or policy_rule_file) and
(policy_type or mode or display_name or description or metadata or parameters)):
ret['comment'] = 'Policy definitions cannot be passed when "policy_rule_json" or "policy_rule_file" is defined!'
return ret
temp_rule = {}
if policy_rule_json:
try:
temp_rule = json.loads(policy_rule_json)
except Exception as exc:
ret['comment'] = 'Unable to load policy rule json! ({0})'.format(exc)
return ret
elif policy_rule_file:
try:
# pylint: disable=unused-variable
sfn, source_sum, comment_ = __salt__['file.get_managed'](
None,
template,
policy_rule_file,
source_hash,
source_hash_name,
None,
None,
None,
__env__,
None,
None,
skip_verify=skip_verify,
**kwargs
)
except Exception as exc:
ret['comment'] = 'Unable to locate policy rule file "{0}"! ({1})'.format(policy_rule_file, exc)
return ret
if not sfn:
ret['comment'] = 'Unable to locate policy rule file "{0}"!)'.format(policy_rule_file)
return ret
try:
with salt.utils.files.fopen(sfn, 'r') as prf:
temp_rule = json.load(prf)
except Exception as exc:
ret['comment'] = 'Unable to load policy rule file "{0}"! ({1})'.format(policy_rule_file, exc)
return ret
if sfn:
salt.utils.files.remove(sfn)
policy_name = name
if policy_rule_json or policy_rule_file:
if temp_rule.get('name'):
policy_name = temp_rule.get('name')
policy_rule = temp_rule.get('properties', {}).get('policyRule')
policy_type = temp_rule.get('properties', {}).get('policyType')
mode = temp_rule.get('properties', {}).get('mode')
display_name = temp_rule.get('properties', {}).get('displayName')
description = temp_rule.get('properties', {}).get('description')
metadata = temp_rule.get('properties', {}).get('metadata')
parameters = temp_rule.get('properties', {}).get('parameters')
policy = __salt__['azurearm_resource.policy_definition_get'](name, azurearm_log_level='info', **connection_auth)
if 'error' not in policy:
if policy_type and policy_type.lower() != policy.get('policy_type', '').lower():
ret['changes']['policy_type'] = {
'old': policy.get('policy_type'),
'new': policy_type
}
if (mode or '').lower() != policy.get('mode', '').lower():
ret['changes']['mode'] = {
'old': policy.get('mode'),
'new': mode
}
if (display_name or '').lower() != policy.get('display_name', '').lower():
ret['changes']['display_name'] = {
'old': policy.get('display_name'),
'new': display_name
}
if (description or '').lower() != policy.get('description', '').lower():
ret['changes']['description'] = {
'old': policy.get('description'),
'new': description
}
rule_changes = __utils__['dictdiffer.deep_diff'](policy.get('policy_rule', {}), policy_rule or {})
if rule_changes:
ret['changes']['policy_rule'] = rule_changes
meta_changes = __utils__['dictdiffer.deep_diff'](policy.get('metadata', {}), metadata or {})
if meta_changes:
ret['changes']['metadata'] = meta_changes
param_changes = __utils__['dictdiffer.deep_diff'](policy.get('parameters', {}), parameters or {})
if param_changes:
ret['changes']['parameters'] = param_changes
if not ret['changes']:
ret['result'] = True
ret['comment'] = 'Policy definition {0} is already present.'.format(name)
return ret
if __opts__['test']:
ret['comment'] = 'Policy definition {0} would be updated.'.format(name)
ret['result'] = None
return ret
else:
ret['changes'] = {
'old': {},
'new': {
'name': policy_name,
'policy_type': policy_type,
'mode': mode,
'display_name': display_name,
'description': description,
'metadata': metadata,
'parameters': parameters,
'policy_rule': policy_rule,
}
}
if __opts__['test']:
ret['comment'] = 'Policy definition {0} would be created.'.format(name)
ret['result'] = None
return ret
# Convert OrderedDict to dict
if isinstance(metadata, dict):
metadata = json.loads(json.dumps(metadata))
if isinstance(parameters, dict):
parameters = json.loads(json.dumps(parameters))
policy_kwargs = kwargs.copy()
policy_kwargs.update(connection_auth)
policy = __salt__['azurearm_resource.policy_definition_create_or_update'](
name=policy_name,
policy_rule=policy_rule,
policy_type=policy_type,
mode=mode,
display_name=display_name,
description=description,
metadata=metadata,
parameters=parameters,
**policy_kwargs
)
if 'error' not in policy:
ret['result'] = True
ret['comment'] = 'Policy definition {0} has been created.'.format(name)
return ret
ret['comment'] = 'Failed to create policy definition {0}! ({1})'.format(name, policy.get('error'))
return ret
def policy_definition_absent(name, connection_auth=None):
'''
.. versionadded:: 2019.2.0
Ensure a policy definition does not exist in the current subscription.
:param name:
Name of the policy definition.
:param connection_auth:
A dict with subscription and authentication parameters to be used in connecting to the
Azure Resource Manager API.
'''
ret = {
'name': name,
'result': False,
'comment': '',
'changes': {}
}
if not isinstance(connection_auth, dict):
ret['comment'] = 'Connection information must be specified via connection_auth dictionary!'
return ret
policy = __salt__['azurearm_resource.policy_definition_get'](name, azurearm_log_level='info', **connection_auth)
if 'error' in policy:
ret['result'] = True
ret['comment'] = 'Policy definition {0} is already absent.'.format(name)
return ret
elif __opts__['test']:
ret['comment'] = 'Policy definition {0} would be deleted.'.format(name)
ret['result'] = None
ret['changes'] = {
'old': policy,
'new': {},
}
return ret
deleted = __salt__['azurearm_resource.policy_definition_delete'](name, **connection_auth)
if deleted:
ret['result'] = True
ret['comment'] = 'Policy definition {0} has been deleted.'.format(name)
ret['changes'] = {
'old': policy,
'new': {}
}
return ret
ret['comment'] = 'Failed to delete policy definition {0}!'.format(name)
return ret
def policy_assignment_present(name, scope, definition_name, display_name=None, description=None, assignment_type=None,
parameters=None, connection_auth=None, **kwargs):
'''
.. versionadded:: 2019.2.0
Ensure a security policy assignment exists.
:param name:
Name of the policy assignment.
:param scope:
The scope of the policy assignment.
:param definition_name:
The name of the policy definition to assign.
:param display_name:
The display name of the policy assignment.
:param description:
The policy assignment description.
:param assignment_type:
The type of policy assignment.
:param parameters:
Required dictionary if a parameter is used in the policy rule.
:param connection_auth:
A dict with subscription and authentication parameters to be used in connecting to the
Azure Resource Manager API.
Example usage:
.. code-block:: yaml
Ensure policy assignment exists:
azurearm_resource.policy_assignment_present:
- name: testassign
- scope: /subscriptions/bc75htn-a0fhsi-349b-56gh-4fghti-f84852
- definition_name: testpolicy
- display_name: Test Assignment
- description: Test assignment for testing assignments.
- connection_auth: {{ profile }}
'''
ret = {
'name': name,
'result': False,
'comment': '',
'changes': {}
}
if not isinstance(connection_auth, dict):
ret['comment'] = 'Connection information must be specified via connection_auth dictionary!'
return ret
policy = __salt__['azurearm_resource.policy_assignment_get'](
name,
scope,
azurearm_log_level='info',
**connection_auth
)
if 'error' not in policy:
if assignment_type and assignment_type.lower() != policy.get('type', '').lower():
ret['changes']['type'] = {
'old': policy.get('type'),
'new': assignment_type
}
if scope.lower() != policy['scope'].lower():
ret['changes']['scope'] = {
'old': policy['scope'],
'new': scope
}
pa_name = policy['policy_definition_id'].split('/')[-1]
if definition_name.lower() != pa_name.lower():
ret['changes']['definition_name'] = {
'old': pa_name,
'new': definition_name
}
if (display_name or '').lower() != policy.get('display_name', '').lower():
ret['changes']['display_name'] = {
'old': policy.get('display_name'),
'new': display_name
}
if (description or '').lower() != policy.get('description', '').lower():
ret['changes']['description'] = {
'old': policy.get('description'),
'new': description
}
param_changes = __utils__['dictdiffer.deep_diff'](policy.get('parameters', {}), parameters or {})
if param_changes:
ret['changes']['parameters'] = param_changes
if not ret['changes']:
ret['result'] = True
ret['comment'] = 'Policy assignment {0} is already present.'.format(name)
return ret
if __opts__['test']:
ret['comment'] = 'Policy assignment {0} would be updated.'.format(name)
ret['result'] = None
return ret
else:
ret['changes'] = {
'old': {},
'new': {
'name': name,
'scope': scope,
'definition_name': definition_name,
'type': assignment_type,
'display_name': display_name,
'description': description,
'parameters': parameters,
}
}
if __opts__['test']:
ret['comment'] = 'Policy assignment {0} would be created.'.format(name)
ret['result'] = None
return ret
if isinstance(parameters, dict):
parameters = json.loads(json.dumps(parameters))
policy_kwargs = kwargs.copy()
policy_kwargs.update(connection_auth)
policy = __salt__['azurearm_resource.policy_assignment_create'](
name=name,
scope=scope,
definition_name=definition_name,
type=assignment_type,
display_name=display_name,
description=description,
parameters=parameters,
**policy_kwargs
)
if 'error' not in policy:
ret['result'] = True
ret['comment'] = 'Policy assignment {0} has been created.'.format(name)
return ret
ret['comment'] = 'Failed to create policy assignment {0}! ({1})'.format(name, policy.get('error'))
return ret
def policy_assignment_absent(name, scope, connection_auth=None):
'''
.. versionadded:: 2019.2.0
Ensure a policy assignment does not exist in the provided scope.
:param name:
Name of the policy assignment.
:param scope:
The scope of the policy assignment.
connection_auth
A dict with subscription and authentication parameters to be used in connecting to the
Azure Resource Manager API.
'''
ret = {
'name': name,
'result': False,
'comment': '',
'changes': {}
}
if not isinstance(connection_auth, dict):
ret['comment'] = 'Connection information must be specified via connection_auth dictionary!'
return ret
policy = __salt__['azurearm_resource.policy_assignment_get'](
name,
scope,
azurearm_log_level='info',
**connection_auth
)
if 'error' in policy:
ret['result'] = True
ret['comment'] = 'Policy assignment {0} is already absent.'.format(name)
return ret
elif __opts__['test']:
ret['comment'] = 'Policy assignment {0} would be deleted.'.format(name)
ret['result'] = None
ret['changes'] = {
'old': policy,
'new': {},
}
return ret
deleted = __salt__['azurearm_resource.policy_assignment_delete'](name, scope, **connection_auth)
if deleted:
ret['result'] = True
ret['comment'] = 'Policy assignment {0} has been deleted.'.format(name)
ret['changes'] = {
'old': policy,
'new': {}
}
return ret
ret['comment'] = 'Failed to delete policy assignment {0}!'.format(name)
return ret