Skip to content

Commit d629079

Browse files
authored
ESXi Config back/restore/reset (#195)
* initial commit * ESXi Config back/restore/reset * remove unnecessary files * fix * remove extra tests * fix tests * fix windows test
1 parent aa9d568 commit d629079

3 files changed

Lines changed: 355 additions & 21 deletions

File tree

src/saltext/vmware/modules/esxi.py

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
# Copyright 2021 VMware, Inc.
22
# SPDX-License: Apache-2.0
33
import logging
4+
import os
45

56
import salt.exceptions
67
import saltext.vmware.utils.common as utils_common
78
import saltext.vmware.utils.esxi as utils_esxi
89
import saltext.vmware.utils.vmware as utils_vmware
910
from salt.defaults import DEFAULT_TARGET_DELIM
1011
from saltext.vmware.utils.connect import get_service_instance
12+
from saltext.vmware.utils.connect import get_username_password
1113

1214
log = logging.getLogger(__name__)
1315

@@ -29,6 +31,8 @@
2931
salt.exceptions.VMwareApiError,
3032
vim.fault.AlreadyExists,
3133
vim.fault.UserNotFound,
34+
salt.exceptions.CommandExecutionError,
35+
vmodl.fault.SystemError,
3236
TypeError,
3337
)
3438

@@ -694,6 +698,231 @@ def get_firewall_config(
694698
raise salt.exceptions.SaltException(str(exc))
695699

696700

701+
def backup_config(
702+
push_file_to_master=False,
703+
http_opts=None,
704+
datacenter_name=None,
705+
cluster_name=None,
706+
host_name=None,
707+
service_instance=None,
708+
):
709+
"""
710+
Backup configuration for matching EXSI hosts.
711+
712+
push_file_to_master
713+
Push the downloaded configuration file to the salt master. (optional)
714+
Refer: https://docs.saltproject.io/en/latest/ref/modules/all/salt.modules.cp.html#salt.modules.cp.push
715+
716+
http_opts
717+
Extra HTTP options to be passed to download from the URL. (optional)
718+
Refer: https://docs.saltproject.io/en/latest/ref/modules/all/salt.modules.http.html#salt.modules.http.query
719+
720+
datacenter_name
721+
Filter by this datacenter name (required when cluster is specified)
722+
723+
cluster_name
724+
Filter by this cluster name (optional)
725+
726+
host_name
727+
Filter by this ESXi hostname (optional)
728+
729+
service_instance
730+
Use this vCenter service connection instance instead of creating a new one. (optional).
731+
732+
.. code-block:: bash
733+
734+
salt * vmware_esxi.backup_config host_name=10.225.0.53 http_opts='{"verify_ssl": False}'
735+
"""
736+
log.debug("Running vmware_esxi.backup_config")
737+
ret = {}
738+
http_opts = http_opts or {}
739+
if not service_instance:
740+
service_instance = get_service_instance(opts=__opts__, pillar=__pillar__)
741+
hosts = utils_esxi.get_hosts(
742+
service_instance=service_instance,
743+
host_names=[host_name] if host_name else None,
744+
cluster_name=cluster_name,
745+
datacenter_name=datacenter_name,
746+
get_all_hosts=host_name is None,
747+
)
748+
749+
try:
750+
for h in hosts:
751+
try:
752+
url = h.configManager.firmwareSystem.BackupFirmwareConfiguration()
753+
url = url.replace("*", h.name)
754+
file_name = os.path.join(__opts__["cachedir"], url.rsplit("/", 1)[1])
755+
data = __salt__["http.query"](url, decode_body=False, **http_opts)
756+
with open(file_name, "wb") as fp:
757+
fp.write(data["body"])
758+
if push_file_to_master:
759+
__salt__["cp.push"](file_name)
760+
ret.setdefault(h.name, {"file_name": file_name})
761+
ret[h.name]["url"] = url
762+
except salt.exceptions.CommandExecutionError as exc:
763+
log.error("Unable to backup configuration for host - %s. Error - %s", h.name, exc)
764+
return ret
765+
except DEFAULT_EXCEPTIONS as exc:
766+
raise salt.exceptions.SaltException(str(exc))
767+
768+
769+
def restore_config(
770+
source_file,
771+
saltenv=None,
772+
http_opts=None,
773+
datacenter_name=None,
774+
cluster_name=None,
775+
host_name=None,
776+
service_instance=None,
777+
):
778+
"""
779+
Restore configuration for matching EXSI hosts.
780+
781+
source_file
782+
Specify the source file from which the configuration is to be restored.
783+
The file can be either on the master, locally on the minion or url.
784+
E.g.: salt://vmware_config.tgz, /tmp/minion1/vmware_config.tgz or
785+
10.225.0.53/downloads/5220da48-552e-5779-703e-5705367bd6d6/configBundle-ESXi-190313806785.eng.vmware.com.tgz
786+
787+
saltenv
788+
Specify the saltenv when the source file needs to be retireved from the master. (optional)
789+
790+
http_opts
791+
Extra HTTP options to be passed to download from the URL. (optional).
792+
Refer: https://docs.saltproject.io/en/latest/ref/modules/all/salt.modules.http.html#salt.modules.http.query
793+
794+
datacenter_name
795+
Filter by this datacenter name (required when cluster is specified)
796+
797+
cluster_name
798+
Filter by this cluster name (optional)
799+
800+
host_name
801+
Filter by this ESXi hostname (optional)
802+
803+
service_instance
804+
Use this vCenter service connection instance instead of creating a new one. (optional).
805+
806+
.. code-block:: bash
807+
808+
salt '*' vmware_esxi.backup_config datacenter_name=dc1 host_name=host1
809+
"""
810+
log.debug("Running vmware_esxi.backup_config")
811+
ret = {}
812+
http_opts = http_opts or {}
813+
if not service_instance:
814+
service_instance = get_service_instance(opts=__opts__, pillar=__pillar__)
815+
hosts = utils_esxi.get_hosts(
816+
service_instance=service_instance,
817+
host_names=[host_name] if host_name else None,
818+
cluster_name=cluster_name,
819+
datacenter_name=datacenter_name,
820+
get_all_hosts=host_name is None,
821+
)
822+
823+
for h in hosts:
824+
try:
825+
data = None
826+
url = h.configManager.firmwareSystem.QueryFirmwareConfigUploadURL().replace("*", h.name)
827+
if source_file.startswith("salt://"):
828+
cached = __salt__["cp.cache_file"](source_file, saltenv=saltenv)
829+
with open(cached, "rb") as fp:
830+
data = fp.read()
831+
elif source_file.startswith("http"):
832+
data = __salt__["http.query"](url, decode_body=False, **http_opts)
833+
else:
834+
with open(source_file, "rb") as fp:
835+
data = fp.read()
836+
username, password = get_username_password(
837+
esxi_host=h.name, opts=__opts__, pillar=__pillar__
838+
)
839+
resp = __salt__["http.query"](
840+
url, data=data, method="PUT", username=username, password=password, **http_opts
841+
)
842+
if "error" in resp:
843+
ret[h.name] = resp["error"]
844+
continue
845+
if not h.runtime.inMaintenanceMode:
846+
log.debug("Host - %s entering maintenance mode", h.name)
847+
utils_common.wait_for_task(
848+
h.EnterMaintenanceMode_Task(timeout=60), h.name, "EnterMaintenanceMode"
849+
)
850+
h.configManager.firmwareSystem.RestoreFirmwareConfiguration(force=False)
851+
ret[h.name] = True
852+
except Exception as exc:
853+
msg = "Unable to restore configuration for host - {}. Error - {}".format(h.name, exc)
854+
log.error(msg)
855+
ret[h.name] = msg
856+
if h.runtime.inMaintenanceMode:
857+
log.debug("Host - %s exiting maintenance mode", h.name)
858+
utils_common.wait_for_task(
859+
h.ExitMaintenanceMode_Task(timeout=60), h.name, "ExitMaintenanceMode"
860+
)
861+
862+
return ret
863+
864+
865+
def reset_config(
866+
datacenter_name=None,
867+
cluster_name=None,
868+
host_name=None,
869+
service_instance=None,
870+
):
871+
"""
872+
Reset configuration for matching EXSI hosts.
873+
874+
datacenter_name
875+
Filter by this datacenter name (required when cluster is specified)
876+
877+
cluster_name
878+
Filter by this cluster name (optional)
879+
880+
host_name
881+
Filter by this ESXi hostname (optional)
882+
883+
service_instance
884+
Use this vCenter service connection instance instead of creating a new one. (optional).
885+
886+
.. code-block:: bash
887+
888+
salt '*' vmware_esxi.reset_config
889+
"""
890+
log.debug("Running vmware_esxi.reset_config")
891+
ret = {}
892+
if not service_instance:
893+
service_instance = get_service_instance(opts=__opts__, pillar=__pillar__)
894+
hosts = utils_esxi.get_hosts(
895+
service_instance=service_instance,
896+
host_names=[host_name] if host_name else None,
897+
cluster_name=cluster_name,
898+
datacenter_name=datacenter_name,
899+
get_all_hosts=host_name is None,
900+
)
901+
902+
for h in hosts:
903+
try:
904+
if not h.runtime.inMaintenanceMode:
905+
log.debug("Host - %s entering maintenance mode", h.name)
906+
utils_common.wait_for_task(
907+
h.EnterMaintenanceMode_Task(timeout=60), h.name, "EnterMaintenanceMode"
908+
)
909+
h.configManager.firmwareSystem.ResetFirmwareToFactoryDefaults()
910+
ret[h.name] = True
911+
except vmodl.fault.HostCommunication as exc:
912+
msg = "Unable to reach host - {}. Error - {}".format(h.name, str(exc))
913+
ret[h.name] = msg
914+
log.error(msg)
915+
except Exception as exc:
916+
msg = "Unable to reset configuration for host - {}. Error - {}".format(h.name, str(exc))
917+
ret[h.name] = msg
918+
log.error(msg)
919+
log.debug("Host - %s exiting maintenance mode", h.name)
920+
utils_common.wait_for_task(
921+
h.ExitMaintenanceMode_Task(timeout=60), h.name, "ExitMaintenanceMode"
922+
)
923+
return ret
924+
925+
697926
def get_dns_config(
698927
datacenter_name=None,
699928
cluster_name=None,

src/saltext/vmware/utils/connect.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,24 @@
2323
log = logging.getLogger(__name__)
2424

2525

26+
def get_username_password(esxi_host, opts=None, pillar=None):
27+
password = (
28+
pillar.get("vmware_config", {}).get("esxi_host", {}).get(esxi_host, {}).get("password")
29+
or opts.get("vmware_config", {}).get("esxi_host", {}).get(esxi_host, {}).get("password")
30+
or os.environ.get("VMWARE_CONFIG_PASSWORD")
31+
or opts.get("vmware_config", {}).get("password")
32+
or pillar.get("vmware_config", {}).get("password")
33+
)
34+
user = (
35+
pillar.get("vmware_config", {}).get("esxi_host", {}).get(esxi_host, {}).get("user")
36+
or opts.get("vmware_config", {}).get("esxi_host", {}).get(esxi_host, {}).get("user")
37+
or os.environ.get("VMWARE_CONFIG_USER")
38+
or opts.get("vmware_config", {}).get("user")
39+
or pillar.get("vmware_config", {}).get("user")
40+
)
41+
return user, password
42+
43+
2644
def get_service_instance(opts=None, pillar=None, esxi_host=None):
2745
"""
2846
Connect to VMware service instance
@@ -70,20 +88,7 @@ def get_service_instance(opts=None, pillar=None, esxi_host=None):
7088
or opts.get("vmware_config", {}).get("host")
7189
or pillar.get("vmware_config", {}).get("host")
7290
)
73-
password = (
74-
pillar.get("vmware_config", {}).get("esxi_host", {}).get(esxi_host, {}).get("password")
75-
or opts.get("vmware_config", {}).get("esxi_host", {}).get(esxi_host, {}).get("password")
76-
or os.environ.get("VMWARE_CONFIG_PASSWORD")
77-
or opts.get("vmware_config", {}).get("password")
78-
or pillar.get("vmware_config", {}).get("password")
79-
)
80-
user = (
81-
pillar.get("vmware_config", {}).get("esxi_host", {}).get(esxi_host, {}).get("user")
82-
or opts.get("vmware_config", {}).get("esxi_host", {}).get(esxi_host, {}).get("user")
83-
or os.environ.get("VMWARE_CONFIG_USER")
84-
or opts.get("vmware_config", {}).get("user")
85-
or pillar.get("vmware_config", {}).get("user")
86-
)
91+
user, password = get_username_password(esxi_host=host, opts=opts, pillar=pillar)
8792
config = {
8893
"host": host,
8994
"password": password,

0 commit comments

Comments
 (0)