1"""
2General management functions for salt, tools like seeing what hosts are up
3and what hosts are down
4"""
5
6
7import logging
8import operator
9import os
10import re
11import subprocess
12import tempfile
13import time
14import urllib.request
15import uuid
16
17import salt.client
18import salt.client.ssh
19import salt.key
20import salt.utils.compat
21import salt.utils.files
22import salt.utils.minions
23import salt.utils.path
24import salt.utils.versions
25import salt.version
26import salt.wheel
27from salt.exceptions import SaltClientError, SaltSystemExit
28
29FINGERPRINT_REGEX = re.compile(r"^([a-f0-9]{2}:){15}([a-f0-9]{2})$")
30
31log = logging.getLogger(__name__)
32
33
34def _ping(tgt, tgt_type, timeout, gather_job_timeout):
35    with salt.client.get_local_client(__opts__["conf_file"]) as client:
36        pub_data = client.run_job(
37            tgt, "test.ping", (), tgt_type, "", timeout, "", listen=True
38        )
39
40        if not pub_data:
41            return pub_data
42
43        log.debug(
44            "manage runner will ping the following minion(s): %s",
45            ", ".join(sorted(pub_data["minions"])),
46        )
47
48        returned = set()
49        for fn_ret in client.get_cli_event_returns(
50            pub_data["jid"],
51            pub_data["minions"],
52            client._get_timeout(timeout),
53            tgt,
54            tgt_type,
55            gather_job_timeout=gather_job_timeout,
56        ):
57
58            if fn_ret:
59                for mid, _ in fn_ret.items():
60                    log.debug("minion '%s' returned from ping", mid)
61                    returned.add(mid)
62
63        not_returned = sorted(set(pub_data["minions"]) - returned)
64        returned = sorted(returned)
65
66        return returned, not_returned
67
68
69def status(
70    output=True, tgt="*", tgt_type="glob", timeout=None, gather_job_timeout=None
71):
72    """
73    .. versionchanged:: 2017.7.0
74
75        The ``expr_form`` argument has been renamed to ``tgt_type``, earlier
76        releases must use ``expr_form``.
77
78    Print the status of all known salt minions
79
80    CLI Example:
81
82    .. code-block:: bash
83
84        salt-run manage.status
85        salt-run manage.status tgt="webservers" tgt_type="nodegroup"
86        salt-run manage.status timeout=5 gather_job_timeout=10
87    """
88    ret = {}
89
90    if not timeout:
91        timeout = __opts__["timeout"]
92    if not gather_job_timeout:
93        gather_job_timeout = __opts__["gather_job_timeout"]
94
95    res = _ping(tgt, tgt_type, timeout, gather_job_timeout)
96    ret["up"], ret["down"] = ([], []) if not res else res
97    return ret
98
99
100def key_regen():
101    """
102    This routine is used to regenerate all keys in an environment. This is
103    invasive! ALL KEYS IN THE SALT ENVIRONMENT WILL BE REGENERATED!!
104
105    The key_regen routine sends a command out to minions to revoke the master
106    key and remove all minion keys, it then removes all keys from the master
107    and prompts the user to restart the master. The minions will all reconnect
108    and keys will be placed in pending.
109
110    After the master is restarted and minion keys are in the pending directory
111    execute a salt-key -A command to accept the regenerated minion keys.
112
113    The master *must* be restarted within 60 seconds of running this command or
114    the minions will think there is something wrong with the keys and abort.
115
116    Only Execute this runner after upgrading minions and master to 0.15.1 or
117    higher!
118
119    CLI Example:
120
121    .. code-block:: bash
122
123        salt-run manage.key_regen
124    """
125    client = salt.client.get_local_client(__opts__["conf_file"])
126    try:
127        client.cmd("*", "saltutil.regen_keys")
128    except SaltClientError as client_error:
129        print(client_error)
130        return False
131
132    for root, _, files in salt.utils.path.os_walk(__opts__["pki_dir"]):
133        for fn_ in files:
134            path = os.path.join(root, fn_)
135            try:
136                os.remove(path)
137            except os.error:
138                pass
139    msg = (
140        "The minion and master keys have been deleted.  Restart the Salt\n"
141        "Master within the next 60 seconds!!!\n\n"
142        "Wait for the minions to reconnect.  Once the minions reconnect\n"
143        "the new keys will appear in pending and will need to be re-\n"
144        "accepted by running:\n"
145        "    salt-key -A\n\n"
146        "Be advised that minions not currently connected to the master\n"
147        "will not be able to reconnect and may require manual\n"
148        "regeneration via a local call to\n"
149        "    salt-call saltutil.regen_keys"
150    )
151    return msg
152
153
154def down(
155    removekeys=False, tgt="*", tgt_type="glob", timeout=None, gather_job_timeout=None
156):
157    """
158    .. versionchanged:: 2017.7.0
159
160        The ``expr_form`` argument has been renamed to ``tgt_type``, earlier
161        releases must use ``expr_form``.
162
163    Print a list of all the down or unresponsive salt minions
164    Optionally remove keys of down minions
165
166    CLI Example:
167
168    .. code-block:: bash
169
170        salt-run manage.down
171        salt-run manage.down removekeys=True
172        salt-run manage.down tgt="webservers" tgt_type="nodegroup"
173    """
174    ret = status(
175        output=False,
176        tgt=tgt,
177        tgt_type=tgt_type,
178        timeout=timeout,
179        gather_job_timeout=gather_job_timeout,
180    ).get("down", [])
181    for minion in ret:
182        if removekeys:
183            wheel = salt.wheel.Wheel(__opts__)
184            wheel.call_func("key.delete", match=minion)
185    return ret
186
187
188def up(
189    tgt="*", tgt_type="glob", timeout=None, gather_job_timeout=None
190):  # pylint: disable=C0103
191    """
192    .. versionchanged:: 2017.7.0
193
194        The ``expr_form`` argument has been renamed to ``tgt_type``, earlier
195        releases must use ``expr_form``.
196
197    Print a list of all of the minions that are up
198
199    CLI Example:
200
201    .. code-block:: bash
202
203        salt-run manage.up
204        salt-run manage.up tgt="webservers" tgt_type="nodegroup"
205        salt-run manage.up timeout=5 gather_job_timeout=10
206    """
207    ret = status(
208        output=False,
209        tgt=tgt,
210        tgt_type=tgt_type,
211        timeout=timeout,
212        gather_job_timeout=gather_job_timeout,
213    ).get("up", [])
214    return ret
215
216
217def list_state(subset=None, show_ip=False):
218    """
219    .. versionadded:: 2015.8.0
220    .. versionchanged:: 2019.2.0
221
222    Print a list of all minions that are up according to Salt's presence
223    detection (no commands will be sent to minions)
224
225    subset : None
226        Pass in a CIDR range to filter minions by IP address.
227
228    show_ip : False
229        Also show the IP address each minion is connecting from.
230
231    CLI Example:
232
233    .. code-block:: bash
234
235        salt-run manage.list_state
236    """
237    # Always return 'present' for 0MQ for now
238    # TODO: implement other states support for 0MQ
239    ckminions = salt.utils.minions.CkMinions(__opts__)
240    minions = ckminions.connected_ids(show_ip=show_ip, subset=subset)
241
242    connected = dict(minions) if show_ip else sorted(minions)
243
244    return connected
245
246
247def list_not_state(subset=None, show_ip=False):
248    """
249    .. versionadded:: 2015.8.0
250    .. versionchanged:: 2019.2.0
251
252    Print a list of all minions that are NOT up according to Salt's presence
253    detection (no commands will be sent to minions)
254
255    subset : None
256        Pass in a CIDR range to filter minions by IP address.
257
258    show_ip : False
259        Also show the IP address each minion is connecting from.
260
261    CLI Example:
262
263    .. code-block:: bash
264
265        salt-run manage.list_not_state
266    """
267    connected = list_state(subset=None, show_ip=show_ip)
268
269    with salt.key.get_key(__opts__) as key:
270        keys = key.list_keys()
271
272        not_connected = []
273        for minion in keys[key.ACC]:
274            if minion not in connected and (subset is None or minion in subset):
275                not_connected.append(minion)
276
277        return not_connected
278
279
280def present(subset=None, show_ip=False):
281    """
282    .. versionchanged:: 2019.2.0
283
284    Print a list of all minions that are up according to Salt's presence
285    detection (no commands will be sent to minions)
286
287    subset : None
288        Pass in a CIDR range to filter minions by IP address.
289
290    show_ip : False
291        Also show the IP address each minion is connecting from.
292
293    CLI Example:
294
295    .. code-block:: bash
296
297        salt-run manage.present
298    """
299    return list_state(subset=subset, show_ip=show_ip)
300
301
302def not_present(subset=None, show_ip=False):
303    """
304    .. versionadded:: 2015.5.0
305    .. versionchanged:: 2019.2.0
306
307    Print a list of all minions that are NOT up according to Salt's presence
308    detection (no commands will be sent)
309
310    subset : None
311        Pass in a CIDR range to filter minions by IP address.
312
313    show_ip : False
314        Also show the IP address each minion is connecting from.
315
316    CLI Example:
317
318    .. code-block:: bash
319
320        salt-run manage.not_present
321    """
322    return list_not_state(subset=subset, show_ip=show_ip)
323
324
325def joined(subset=None, show_ip=False):
326    """
327    .. versionadded:: 2015.8.0
328    .. versionchanged:: 2019.2.0
329
330    Print a list of all minions that are up according to Salt's presence
331    detection (no commands will be sent to minions)
332
333    subset : None
334        Pass in a CIDR range to filter minions by IP address.
335
336    show_ip : False
337        Also show the IP address each minion is connecting from.
338
339    CLI Example:
340
341    .. code-block:: bash
342
343        salt-run manage.joined
344    """
345    return list_state(subset=subset, show_ip=show_ip)
346
347
348def not_joined(subset=None, show_ip=False):
349    """
350    .. versionadded:: 2015.8.0
351    .. versionchanged:: 2019.2.0
352
353    Print a list of all minions that are NOT up according to Salt's presence
354    detection (no commands will be sent)
355
356    subset : None
357        Pass in a CIDR range to filter minions by IP address.
358
359    show_ip : False
360        Also show the IP address each minion is connecting from.
361
362    CLI Example:
363
364    .. code-block:: bash
365
366        salt-run manage.not_joined
367    """
368    return list_not_state(subset=subset, show_ip=show_ip)
369
370
371def allowed(subset=None, show_ip=False):
372    """
373    .. versionadded:: 2015.8.0
374    .. versionchanged:: 2019.2.0
375
376    Print a list of all minions that are up according to Salt's presence
377    detection (no commands will be sent to minions)
378
379    subset : None
380        Pass in a CIDR range to filter minions by IP address.
381
382    show_ip : False
383        Also show the IP address each minion is connecting from.
384
385    CLI Example:
386
387    .. code-block:: bash
388
389        salt-run manage.allowed
390    """
391    return list_state(subset=subset, show_ip=show_ip)
392
393
394def not_allowed(subset=None, show_ip=False):
395    """
396    .. versionadded:: 2015.8.0
397    .. versionchanged:: 2019.2.0
398
399    Print a list of all minions that are NOT up according to Salt's presence
400    detection (no commands will be sent)
401
402    subset : None
403        Pass in a CIDR range to filter minions by IP address.
404
405    show_ip : False
406        Also show the IP address each minion is connecting from.
407
408    CLI Example:
409
410    .. code-block:: bash
411
412        salt-run manage.not_allowed
413    """
414    return list_not_state(subset=subset, show_ip=show_ip)
415
416
417def alived(subset=None, show_ip=False):
418    """
419    .. versionadded:: 2015.8.0
420    .. versionchanged:: 2019.2.0
421
422    Print a list of all minions that are up according to Salt's presence
423    detection (no commands will be sent to minions)
424
425    subset : None
426        Pass in a CIDR range to filter minions by IP address.
427
428    show_ip : False
429        Also show the IP address each minion is connecting from.
430
431    CLI Example:
432
433    .. code-block:: bash
434
435        salt-run manage.alived
436    """
437    return list_state(subset=subset, show_ip=show_ip)
438
439
440def not_alived(subset=None, show_ip=False):
441    """
442    .. versionadded:: 2015.8.0
443    .. versionchanged:: 2019.2.0
444
445    Print a list of all minions that are NOT up according to Salt's presence
446    detection (no commands will be sent)
447
448    subset : None
449        Pass in a CIDR range to filter minions by IP address.
450
451    show_ip : False
452        Also show the IP address each minion is connecting from.
453
454    CLI Example:
455
456    .. code-block:: bash
457
458        salt-run manage.not_alived
459    """
460    return list_not_state(subset=subset, show_ip=show_ip)
461
462
463def reaped(subset=None, show_ip=False):
464    """
465    .. versionadded:: 2015.8.0
466    .. versionchanged:: 2019.2.0
467
468    Print a list of all minions that are up according to Salt's presence
469    detection (no commands will be sent to minions)
470
471    subset : None
472        Pass in a CIDR range to filter minions by IP address.
473
474    show_ip : False
475        Also show the IP address each minion is connecting from.
476
477    CLI Example:
478
479    .. code-block:: bash
480
481        salt-run manage.reaped
482    """
483    return list_state(subset=subset, show_ip=show_ip)
484
485
486def not_reaped(subset=None, show_ip=False):
487    """
488    .. versionadded:: 2015.8.0
489    .. versionchanged:: 2019.2.0
490
491    Print a list of all minions that are NOT up according to Salt's presence
492    detection (no commands will be sent)
493
494    subset : None
495        Pass in a CIDR range to filter minions by IP address.
496
497    show_ip : False
498        Also show the IP address each minion is connecting from.
499
500    CLI Example:
501
502    .. code-block:: bash
503
504        salt-run manage.not_reaped
505    """
506    return list_not_state(subset=subset, show_ip=show_ip)
507
508
509def safe_accept(target, tgt_type="glob"):
510    """
511    .. versionchanged:: 2017.7.0
512
513        The ``expr_form`` argument has been renamed to ``tgt_type``, earlier
514        releases must use ``expr_form``.
515
516    Accept a minion's public key after checking the fingerprint over salt-ssh
517
518    CLI Example:
519
520    .. code-block:: bash
521
522        salt-run manage.safe_accept my_minion
523        salt-run manage.safe_accept minion1,minion2 tgt_type=list
524    """
525    ssh_client = salt.client.ssh.client.SSHClient()
526    ret = ssh_client.cmd(target, "key.finger", tgt_type=tgt_type)
527
528    failures = {}
529    for minion, finger in ret.items():
530        if not FINGERPRINT_REGEX.match(finger):
531            failures[minion] = finger
532        else:
533            with salt.key.Key(__opts__) as salt_key:
534                fingerprints = salt_key.finger(minion)
535            accepted = fingerprints.get("minions", {})
536            pending = fingerprints.get("minions_pre", {})
537            if minion in accepted:
538                del ret[minion]
539                continue
540            elif minion not in pending:
541                failures[minion] = "Minion key {} not found by salt-key".format(minion)
542            elif pending[minion] != finger:
543                failures[
544                    minion
545                ] = "Minion key {} does not match the key in salt-key: {}".format(
546                    finger, pending[minion]
547                )
548            else:
549                subprocess.call(["salt-key", "-qya", minion])
550
551        if minion in failures:
552            del ret[minion]
553
554    if failures:
555        print("safe_accept failed on the following minions:")
556        for minion, message in failures.items():
557            print(minion)
558            print("-" * len(minion))
559            print(message)
560            print("")
561
562    __jid_event__.fire_event(
563        {"message": "Accepted {:d} keys".format(len(ret))}, "progress"
564    )
565    return ret, failures
566
567
568def versions():
569    """
570    Check the version of active minions
571
572    CLI Example:
573
574    .. code-block:: bash
575
576        salt-run manage.versions
577    """
578    ret = {}
579    client = salt.client.get_local_client(__opts__["conf_file"])
580    try:
581        minions = client.cmd("*", "test.version", timeout=__opts__["timeout"])
582    except SaltClientError as client_error:
583        print(client_error)
584        return ret
585
586    labels = {
587        -2: "Minion offline",
588        -1: "Minion requires update",
589        0: "Up to date",
590        1: "Minion newer than master",
591        2: "Master",
592    }
593
594    version_status = {}
595
596    master_version = salt.version.__saltstack_version__
597
598    for minion in minions:
599        if not minions[minion]:
600            minion_version = False
601            ver_diff = -2
602        else:
603            minion_version = salt.version.SaltStackVersion.parse(minions[minion])
604            ver_diff = salt.utils.compat.cmp(minion_version, master_version)
605
606        if ver_diff not in version_status:
607            version_status[ver_diff] = {}
608        if minion_version:
609            version_status[ver_diff][minion] = minion_version.string
610        else:
611            version_status[ver_diff][minion] = minion_version
612
613    # Add version of Master to output
614    version_status[2] = master_version.string
615
616    for key in version_status:
617        if key == 2:
618            ret[labels[key]] = version_status[2]
619        else:
620            for minion in sorted(version_status[key]):
621                ret.setdefault(labels[key], {})[minion] = version_status[key][minion]
622    return ret
623
624
625def bootstrap(
626    version="develop",
627    script=None,
628    hosts="",
629    script_args="",
630    roster="flat",
631    ssh_user=None,
632    ssh_password=None,
633    ssh_priv_key=None,
634    tmp_dir="/tmp/.bootstrap",
635    http_backend="tornado",
636):
637    """
638    Bootstrap minions with salt-bootstrap
639
640    version : develop
641        Git tag of version to install
642
643    script : https://bootstrap.saltstack.com
644        URL containing the script to execute
645
646    hosts
647        Comma-separated hosts [example: hosts='host1.local,host2.local']. These
648        hosts need to exist in the specified roster.
649
650    script_args
651        Any additional arguments that you want to pass to the script.
652
653        .. versionadded:: 2016.11.0
654
655    roster : flat
656        The roster to use for Salt SSH. More information about roster files can
657        be found in :ref:`Salt's Roster Documentation <ssh-roster>`.
658
659        A full list of roster types, see the :ref:`builtin roster modules <all-salt.roster>`
660        documentation.
661
662        .. versionadded:: 2016.11.0
663
664    ssh_user
665        If ``user`` isn't found in the ``roster``, a default SSH user can be set here.
666        Keep in mind that ``ssh_user`` will not override the roster ``user`` value if
667        it is already defined.
668
669        .. versionadded:: 2016.11.0
670
671    ssh_password
672        If ``passwd`` isn't found in the ``roster``, a default SSH password can be set
673        here. Keep in mind that ``ssh_password`` will not override the roster ``passwd``
674        value if it is already defined.
675
676        .. versionadded:: 2016.11.0
677
678    ssh_privkey
679        If ``priv`` isn't found in the ``roster``, a default SSH private key can be set
680        here. Keep in mind that ``ssh_password`` will not override the roster ``passwd``
681        value if it is already defined.
682
683        .. versionadded:: 2016.11.0
684
685    tmp_dir : /tmp/.bootstrap
686        The temporary directory to download the bootstrap script in. This
687        directory will have ``-<uuid4>`` appended to it. For example:
688        ``/tmp/.bootstrap-a19a728e-d40a-4801-aba9-d00655c143a7/``
689
690        .. versionadded:: 2016.11.0
691
692    http_backend : tornado
693        The backend library to use to download the script. If you need to use
694        a ``file:///`` URL, then you should set this to ``urllib2``.
695
696        .. versionadded:: 2016.11.0
697
698    CLI Example:
699
700    .. code-block:: bash
701
702        salt-run manage.bootstrap hosts='host1,host2'
703        salt-run manage.bootstrap hosts='host1,host2' version='v0.17'
704        salt-run manage.bootstrap hosts='host1,host2' version='v0.17' script='https://bootstrap.saltstack.com/develop'
705    """
706    if script is None:
707        script = "https://bootstrap.saltstack.com"
708
709    client_opts = __opts__.copy()
710    if roster is not None:
711        client_opts["roster"] = roster
712
713    if ssh_user is not None:
714        client_opts["ssh_user"] = ssh_user
715
716    if ssh_password is not None:
717        client_opts["ssh_passwd"] = ssh_password
718
719    if ssh_priv_key is not None:
720        client_opts["ssh_priv"] = ssh_priv_key
721
722    for host in hosts.split(","):
723        client_opts["tgt"] = host
724        client_opts["selected_target_option"] = "glob"
725        tmp_dir = "{}-{}/".format(tmp_dir.rstrip("/"), uuid.uuid4())
726        deploy_command = os.path.join(tmp_dir, "deploy.sh")
727        try:
728            client_opts["argv"] = ["file.makedirs", tmp_dir, "mode=0700"]
729            salt.client.ssh.SSH(client_opts).run()
730            client_opts["argv"] = [
731                "http.query",
732                script,
733                "backend={}".format(http_backend),
734                "text_out={}".format(deploy_command),
735            ]
736            salt.client.ssh.SSH(client_opts).run()
737            client_opts["argv"] = [
738                "cmd.run",
739                " ".join(["sh", deploy_command, script_args]),
740                "python_shell=False",
741            ]
742            salt.client.ssh.SSH(client_opts).run()
743            client_opts["argv"] = ["file.remove", tmp_dir]
744            salt.client.ssh.SSH(client_opts).run()
745        except SaltSystemExit as exc:
746            log.error(str(exc))
747
748
749def bootstrap_psexec(
750    hosts="",
751    master=None,
752    version=None,
753    arch="win32",
754    installer_url=None,
755    username=None,
756    password=None,
757):
758    """
759    Bootstrap Windows minions via PsExec.
760
761    hosts
762        Comma separated list of hosts to deploy the Windows Salt minion.
763
764    master
765        Address of the Salt master passed as an argument to the installer.
766
767    version
768        Point release of installer to download. Defaults to the most recent.
769
770    arch
771        Architecture of installer to download. Defaults to win32.
772
773    installer_url
774        URL of minion installer executable. Defaults to the latest version from
775        https://repo.saltproject.io/windows/
776
777    username
778        Optional user name for login on remote computer.
779
780    password
781        Password for optional username. If omitted, PsExec will prompt for one
782        to be entered for each host.
783
784    CLI Example:
785
786    .. code-block:: bash
787
788        salt-run manage.bootstrap_psexec hosts='host1,host2'
789        salt-run manage.bootstrap_psexec hosts='host1,host2' version='0.17' username='DOMAIN\\Administrator'
790        salt-run manage.bootstrap_psexec hosts='host1,host2' installer_url='http://exampledomain/salt-installer.exe'
791    """
792
793    if not installer_url:
794        base_url = "https://repo.saltproject.io/windows/"
795        source = urllib.request.urlopen(base_url).read()
796        salty_rx = re.compile(
797            '>(Salt-Minion-(.+?)-(.+)-Setup.exe)</a></td><td align="right">(.*?)\\s*<'
798        )
799        source_list = sorted(
800            [
801                [path, ver, plat, time.strptime(date, "%d-%b-%Y %H:%M")]
802                for path, ver, plat, date in salty_rx.findall(source)
803            ],
804            key=operator.itemgetter(3),
805            reverse=True,
806        )
807        if version:
808            source_list = [s for s in source_list if s[1] == version]
809        if arch:
810            source_list = [s for s in source_list if s[2] == arch]
811
812        if not source_list:
813            return -1
814
815        version = source_list[0][1]
816        arch = source_list[0][2]
817        installer_url = base_url + source_list[0][0]
818
819    # It's no secret that Windows is notoriously command-line hostile.
820    # Win 7 and newer can use PowerShell out of the box, but to reach
821    # all those XP and 2K3 machines we must suppress our gag-reflex
822    # and use VB!
823
824    # The following script was borrowed from an informative article about
825    # downloading exploit payloads for malware. Nope, no irony here.
826    # http://www.greyhathacker.net/?p=500
827    vb_script = """strFileURL = "{0}"
828strHDLocation = "{1}"
829Set objXMLHTTP = CreateObject("MSXML2.XMLHTTP")
830objXMLHTTP.open "GET", strFileURL, false
831objXMLHTTP.send()
832If objXMLHTTP.Status = 200 Then
833Set objADOStream = CreateObject("ADODB.Stream")
834objADOStream.Open
835objADOStream.Type = 1
836objADOStream.Write objXMLHTTP.ResponseBody
837objADOStream.Position = 0
838objADOStream.SaveToFile strHDLocation
839objADOStream.Close
840Set objADOStream = Nothing
841End if
842Set objXMLHTTP = Nothing
843Set objShell = CreateObject("WScript.Shell")
844objShell.Exec("{1}{2}")"""
845
846    vb_saltexec = "saltinstall.exe"
847    vb_saltexec_args = " /S /minion-name=%COMPUTERNAME%"
848    if master:
849        vb_saltexec_args += " /master={}".format(master)
850
851    # One further thing we need to do; the Windows Salt minion is pretty
852    # self-contained, except for the Microsoft Visual C++ 2008 runtime.
853    # It's tiny, so the bootstrap will attempt a silent install.
854    vb_vcrunexec = "vcredist.exe"
855    if arch == "AMD64":
856        vb_vcrun = vb_script.format(
857            "http://download.microsoft.com/download/d/2/4/d242c3fb-da5a-4542-ad66-f9661d0a8d19/vcredist_x64.exe",
858            vb_vcrunexec,
859            " /q",
860        )
861    else:
862        vb_vcrun = vb_script.format(
863            "http://download.microsoft.com/download/d/d/9/dd9a82d0-52ef-40db-8dab-795376989c03/vcredist_x86.exe",
864            vb_vcrunexec,
865            " /q",
866        )
867
868    vb_salt = vb_script.format(installer_url, vb_saltexec, vb_saltexec_args)
869
870    # PsExec doesn't like extra long arguments; save the instructions as a batch
871    # file so we can fire it over for execution.
872
873    # First off, change to the local temp directory, stop salt-minion (if
874    # running), and remove the master's public key.
875    # This is to accommodate for reinstalling Salt over an old or broken build,
876    # e.g. if the master address is changed, the salt-minion process will fail
877    # to authenticate and quit; which means infinite restarts under Windows.
878    batch = (
879        "cd /d %TEMP%\nnet stop salt-minion\ndel"
880        " c:\\salt\\conf\\pki\\minion\\minion_master.pub\n"
881    )
882
883    # Speaking of command-line hostile, cscript only supports reading a script
884    # from a file. Glue it together line by line.
885    for x, y in ((vb_vcrunexec, vb_vcrun), (vb_saltexec, vb_salt)):
886        vb_lines = y.split("\n")
887        batch += (
888            "\ndel "
889            + x
890            + "\n@echo "
891            + vb_lines[0]
892            + "  >"
893            + x
894            + ".vbs\n@echo "
895            + ("  >>" + x + ".vbs\n@echo ").join(vb_lines[1:])
896            + "  >>"
897            + x
898            + ".vbs\ncscript.exe /NoLogo "
899            + x
900            + ".vbs"
901        )
902
903    batch_path = tempfile.mkstemp(suffix=".bat")[1]
904    with salt.utils.files.fopen(batch_path, "wb") as batch_file:
905        batch_file.write(batch)
906
907    for host in hosts.split(","):
908        argv = ["psexec", "\\\\" + host]
909        if username:
910            argv += ["-u", username]
911            if password:
912                argv += ["-p", password]
913        argv += ["-h", "-c", batch_path]
914        subprocess.call(argv)
915