Skip to content

Commit 3824e7a

Browse files
zruzhinovjoechainzwaynew
authored
[CoE] vCenter Storage Policies - moodule, state, and tests (#333)
* vCenter Storage Policies - moodule, state, and tests * Add changelog * Fix 3.7 3.8 python compatibility * Fix dict merge * Fix changes from PR review * Fix vMomi create/update of policy. Fix drift report lib * Pass present state (#337) * create ntp config state * Update src/saltext/vmware/states/esxi.py Co-authored-by: Wayne Werner <waynejwerner@gmail.com> * white space * changes * password present state Co-authored-by: Wayne Werner <waynejwerner@gmail.com> * Fixes for #333 (review) Co-authored-by: Joey Gibson <56270734+joechainz@users.noreply.github.com> Co-authored-by: Wayne Werner <waynejwerner@gmail.com>
1 parent 7fe6e9b commit 3824e7a

11 files changed

Lines changed: 1709 additions & 0 deletions

File tree

changelog/333.added

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Added modules storage_policy.find and storage_policy.save.
2+
Added state storage_policy.config.

docs/ref/modules/all.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Execution Modules
3131
saltext.vmware.modules.nsxt_transport_zone
3232
saltext.vmware.modules.nsxt_uplink_profiles
3333
saltext.vmware.modules.ssl_adapter
34+
saltext.vmware.modules.storage_policies
3435
saltext.vmware.modules.tag
3536
saltext.vmware.modules.vm
3637
saltext.vmware.modules.vmc_dhcp_profiles
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
saltext.vmware.modules.storage_policies
3+
=======================================
4+
5+
.. automodule:: saltext.vmware.modules.storage_policies
6+
:members:

docs/ref/states/all.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ State Modules
2525
saltext.vmware.states.nsxt_transport_node_profiles
2626
saltext.vmware.states.nsxt_transport_zone
2727
saltext.vmware.states.nsxt_uplink_profiles
28+
saltext.vmware.states.storage_policies
2829
saltext.vmware.states.tag
2930
saltext.vmware.states.vm
3031
saltext.vmware.states.vmc_dhcp_profiles
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
saltext.vmware.states.storage_policies
3+
======================================
4+
5+
.. automodule:: saltext.vmware.states.storage_policies
6+
:members:

src/saltext/vmware/modules/storage_policies.py

Lines changed: 449 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
import json
3+
import logging
4+
5+
import saltext.vmware.modules.storage_policies as policy_module
6+
import saltext.vmware.utils.common as utils_common
7+
import saltext.vmware.utils.connect as connect
8+
import saltext.vmware.utils.drift as drift
9+
10+
log = logging.getLogger(__name__)
11+
12+
try:
13+
from pyVmomi import vim
14+
15+
HAS_PYVMOMI = True
16+
except ImportError:
17+
HAS_PYVMOMI = False
18+
19+
20+
__virtualname__ = "storage_policy"
21+
__proxyenabled__ = ["storage_policy"]
22+
23+
24+
def __virtual__():
25+
if not HAS_PYVMOMI:
26+
return False, "Unable to import pyVmomi module."
27+
return __virtualname__
28+
29+
30+
def config(name, config, service_instance=None, profile=None):
31+
"""
32+
Get/Set storage policies configuration based on drift report.
33+
34+
name
35+
Name of configuration. (required).
36+
37+
config
38+
List of objects with configuration values. (required).
39+
40+
.. code-block:: yaml
41+
42+
storage_policies_drift:
43+
storage_policy.config:
44+
- profile: vcenter
45+
- config:
46+
- policyName: Performance - Thick
47+
constraints:
48+
- name: VSAN
49+
replicaPreference: 1 failures - RAID-1 (Mirroring)
50+
hostFailuresToTolerate: 2
51+
checksumDisabled: true
52+
stripeWidth: 2
53+
proportionalCapacity: 50
54+
cacheReservation: 0
55+
- policyName: New VM Storage Policy
56+
constraints:
57+
- name: VSAN
58+
hostFailuresToTolerate: 2
59+
replicaPreference: 2 failures - RAID-1 (Mirroring)
60+
proportionalCapacity: 1
61+
62+
service_instance
63+
Use this vCenter service connection instance instead of creating a new one. (optional).
64+
65+
profile
66+
Profile to use (optional)
67+
"""
68+
69+
service_instance = service_instance or connect.get_service_instance(
70+
config=__opts__, profile=profile
71+
)
72+
ret = {"name": name, "changes": {}, "result": None, "comment": ""}
73+
74+
# Clone config input to a Map.
75+
# Can be used to transform input to internal objects and do validation if needed
76+
new_config = {}
77+
for policy_config in config:
78+
# Create full representation of the object, default or empty values
79+
new_policy_config = {"constraints": {}}
80+
# Transform / Validate input vs object, e.g. constraints section
81+
if "constraints" in policy_config:
82+
for constraint in policy_config["constraints"]:
83+
subProfileName = constraint["name"]
84+
new_policy_config["constraints"][subProfileName] = {}
85+
for key in constraint.keys():
86+
if key != "name":
87+
new_policy_config["constraints"][subProfileName][key] = constraint[key]
88+
new_config[policy_config["policyName"]] = new_policy_config
89+
90+
log.debug("---------------NEW--------------")
91+
log.debug(json.dumps(new_config, indent=2))
92+
log.debug("---------------NEW--------------")
93+
94+
# Get all policies from vCenter, the objects are VMOMI objects
95+
old_configs = policy_module.find(
96+
policy_name=None, service_instance=service_instance, profile=profile
97+
)
98+
99+
# make JSON representation of current policies
100+
# old_configs holds only the rules that are in the scope of interest (provided in argument config_input)
101+
old_config = {}
102+
for policy in old_configs:
103+
if policy["policyName"] not in new_config.keys():
104+
continue
105+
106+
policy_json = {"constraints": {}}
107+
108+
for constraint in policy["constraints"]:
109+
subProfileName = constraint["name"]
110+
policy_json["constraints"][subProfileName] = {}
111+
for key in constraint.keys():
112+
if key != "name":
113+
policy_json["constraints"][subProfileName][key] = constraint[key]
114+
115+
old_config[policy["policyName"]] = policy_json
116+
117+
log.debug("--------------OLD---------------")
118+
log.debug(json.dumps(old_config, indent=2))
119+
log.debug("--------------OLD---------------")
120+
121+
# Find rules changes
122+
changes = []
123+
rule_diff = drift.drift_report(old_config, new_config, diff_level=0)
124+
policies_diff = json.loads(json.dumps(rule_diff)) # clone object
125+
if policies_diff is not None:
126+
ret["changes"] = policies_diff
127+
128+
log.debug("==============DRIFT===============")
129+
log.debug(json.dumps(rule_diff, indent=2))
130+
log.debug("==============DRIFT===============")
131+
132+
# add changes for process if not dry-run
133+
for policy_name in policies_diff:
134+
new_policy = policies_diff[policy_name]["new"]
135+
changes.append({**{"name": policy_name}, **new_policy})
136+
137+
# If it's not dry-run and has changes, then apply changes
138+
if not __opts__["test"] and changes:
139+
success = True
140+
141+
log.debug("===============CHANGES==============")
142+
log.debug(json.dumps(changes, indent=2))
143+
log.debug("===============CHANGES==============")
144+
145+
comments = {}
146+
for change in changes:
147+
try:
148+
# convert change report to configuration object
149+
new_constraints = []
150+
for constr_name in change["constraints"].keys():
151+
constr_props = change["constraints"][constr_name].copy()
152+
constr_props["name"] = constr_name
153+
new_constraints.append(constr_props)
154+
update = {"policyName": change["name"], "constraints": new_constraints}
155+
# save policy
156+
policy_module.save(update, service_instance, profile)
157+
comments[change["name"]] = {
158+
"status": "SUCCESS",
159+
"message": f"Storage policy '{change['name']}' has been changed successfully.",
160+
}
161+
except Exception as err:
162+
success = False
163+
comments[change["name"]] = {
164+
"status": "FAILURE",
165+
"message": f"Error occured while save storage policy '{change['name']}': {err}",
166+
}
167+
168+
ret["comment"] = comments # it's more readable if passed as object
169+
ret["result"] = success # at least one success
170+
171+
return ret

src/saltext/vmware/utils/drift.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Copyright 2021 VMware, Inc.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""
4+
Common functions used across modules
5+
"""
6+
import json
7+
8+
9+
def drift_report(obj1, obj2, diff_level=None):
10+
"""
11+
Finds the drift between two objects/dictionaries
12+
13+
obj1
14+
First dictionary
15+
obj2
16+
Second dictionary
17+
diff_level
18+
Defines on what tree level to show the drift.
19+
If not specified the changes are shown in the last/leaf level.
20+
21+
Drift level None - all leaf nodes changes:
22+
o
23+
|_
24+
| x
25+
|__
26+
| |_
27+
x | x
28+
|__
29+
| |_
30+
x x
31+
32+
Drift level 0 - changes in first level of config tree:
33+
o
34+
|
35+
x...
36+
|
37+
x...
38+
39+
Drift level 1 - changes in second level of config tree:
40+
o
41+
|_
42+
| x...
43+
|_
44+
x...
45+
46+
Drift level 2 - changes in third level of config tree:
47+
o
48+
|_
49+
| |_
50+
|_ x
51+
|_
52+
x
53+
"""
54+
result_diffs = []
55+
_drift_recurse_(obj1, obj2, result_diffs)
56+
57+
result_tree = {}
58+
for tup in result_diffs:
59+
_drift_tree_(tup, result_tree)
60+
61+
if diff_level is not None and isinstance(result_tree, dict):
62+
result_subtree = {}
63+
_drift_subtree_(result_tree, diff_level, result_subtree)
64+
return result_subtree
65+
66+
return result_tree
67+
68+
69+
def _drift_tree_(diffs, result_tree):
70+
branch = result_tree
71+
for i in range(len(diffs)):
72+
if i < (len(diffs) - 2):
73+
if diffs[i] not in branch:
74+
branch[diffs[i]] = {}
75+
branch = branch[diffs[i]]
76+
elif i == (len(diffs) - 2):
77+
branch[diffs[i]] = diffs[i + 1]
78+
79+
80+
def _drift_subtree_(result_tree, diff_level, result_subtree, level=0, new_subtree=0):
81+
for k in result_tree.keys():
82+
if isinstance(result_tree[k], dict):
83+
if level == diff_level:
84+
result_subtree[k] = {"old": {}, "new": {}}
85+
_drift_subtree_(
86+
result_tree[k], diff_level, result_subtree[k]["old"], level + 1, new_subtree=1
87+
)
88+
_drift_subtree_(
89+
result_tree[k], diff_level, result_subtree[k]["new"], level + 1, new_subtree=2
90+
)
91+
else:
92+
if k not in result_subtree:
93+
result_subtree[k] = {}
94+
_drift_subtree_(
95+
result_tree[k], diff_level, result_subtree[k], level + 1, new_subtree
96+
)
97+
else:
98+
if new_subtree == 1:
99+
result_subtree[k] = result_tree[k][0]
100+
elif new_subtree == 2:
101+
result_subtree[k] = result_tree[k][1]
102+
else:
103+
result_subtree[k] = {"old": result_tree[k][0], "new": result_tree[k][1]}
104+
105+
106+
def _drift_recurse_(obj1, obj2, result, keys=[]):
107+
if isinstance(obj1, dict) and isinstance(obj2, dict):
108+
if not keys:
109+
# use symmetric difference for first level only
110+
for k in set(obj1).symmetric_difference(obj2):
111+
if k in obj1:
112+
# first level element value should be dict
113+
result.append(tuple(keys) + (k,) + ((obj1[k], {}),))
114+
else:
115+
# first level element value should be dict
116+
result.append(tuple(keys) + (k,) + (({}, obj2[k]),))
117+
else:
118+
# otherwise make difference only for added elements in obj2
119+
for k in set(obj1).symmetric_difference(obj2):
120+
if k in obj2:
121+
# only for complex types - dict, list
122+
if isinstance(obj2[k], dict):
123+
result.append(tuple(keys) + (k,) + (({}, obj2[k]),))
124+
elif isinstance(obj2[k], (list, tuple)):
125+
result.append(tuple(keys) + (k,) + (([], obj2[k]),))
126+
for k in set(obj1).intersection(obj2):
127+
_drift_recurse_(obj1[k], obj2[k], result, keys=keys + [k])
128+
else:
129+
if isinstance(obj1, (list, tuple)) and isinstance(obj2, (list, tuple)):
130+
if set(obj1) != set(obj2):
131+
result.append(tuple(keys) + ((obj1, obj2),))
132+
elif obj1 != obj2:
133+
result.append(tuple(keys) + ((obj1, obj2),))

0 commit comments

Comments
 (0)