123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448 |
- #!/usr/bin/env python
- # -*- coding: utf-8 -*-
- """
- The minionswarm script will start a group of salt minions with different ids
- on a single system to test scale capabilities
- """
- # pylint: disable=resource-leakage
- # Import Python Libs
- from __future__ import absolute_import, print_function
- import hashlib
- import optparse
- import os
- import random
- import shutil
- import signal
- import subprocess
- import sys
- import tempfile
- import time
- import uuid
- # Import salt libs
- import salt
- import salt.utils.files
- import salt.utils.yaml
- import tests.support.runtests
- # Import third party libs
- from salt.ext import six
- from salt.ext.six.moves import range # pylint: disable=import-error,redefined-builtin
- OSES = [
- "Arch",
- "Ubuntu",
- "Debian",
- "CentOS",
- "Fedora",
- "Gentoo",
- "AIX",
- "Solaris",
- ]
- VERS = [
- "2014.1.6",
- "2014.7.4",
- "2015.5.5",
- "2015.8.0",
- ]
- def parse():
- """
- Parse the cli options
- """
- parser = optparse.OptionParser()
- parser.add_option(
- "-m",
- "--minions",
- dest="minions",
- default=5,
- type="int",
- help="The number of minions to make",
- )
- parser.add_option(
- "-M",
- action="store_true",
- dest="master_too",
- default=False,
- help="Run a local master and tell the minions to connect to it",
- )
- parser.add_option(
- "--master",
- dest="master",
- default="salt",
- help="The location of the salt master that this swarm will serve",
- )
- parser.add_option(
- "--name",
- "-n",
- dest="name",
- default="ms",
- help=(
- "Give the minions an alternative id prefix, this is used "
- "when minions from many systems are being aggregated onto "
- "a single master"
- ),
- )
- parser.add_option(
- "--rand-os",
- dest="rand_os",
- default=False,
- action="store_true",
- help="Each Minion claims a different os grain",
- )
- parser.add_option(
- "--rand-ver",
- dest="rand_ver",
- default=False,
- action="store_true",
- help="Each Minion claims a different version grain",
- )
- parser.add_option(
- "--rand-machine-id",
- dest="rand_machine_id",
- default=False,
- action="store_true",
- help="Each Minion claims a different machine id grain",
- )
- parser.add_option(
- "--rand-uuid",
- dest="rand_uuid",
- default=False,
- action="store_true",
- help="Each Minion claims a different UUID grain",
- )
- parser.add_option(
- "-k",
- "--keep-modules",
- dest="keep",
- default="",
- help="A comma delimited list of modules to enable",
- )
- parser.add_option(
- "-f",
- "--foreground",
- dest="foreground",
- default=False,
- action="store_true",
- help=(
- "Run the minions with debug output of the swarm going to " "the terminal"
- ),
- )
- parser.add_option(
- "--temp-dir",
- dest="temp_dir",
- default=None,
- help="Place temporary files/directories here",
- )
- parser.add_option(
- "--no-clean",
- action="store_true",
- default=False,
- help="Don't cleanup temporary files/directories",
- )
- parser.add_option(
- "--root-dir",
- dest="root_dir",
- default=None,
- help="Override the minion root_dir config",
- )
- parser.add_option(
- "--transport",
- dest="transport",
- default="zeromq",
- help="Declare which transport to use, default is zeromq",
- )
- parser.add_option(
- "--start-delay",
- dest="start_delay",
- default=0.0,
- type="float",
- help="Seconds to wait between minion starts",
- )
- parser.add_option(
- "-c",
- "--config-dir",
- default="",
- help=("Pass in a configuration directory containing base configuration."),
- )
- parser.add_option("-u", "--user", default=tests.support.runtests.this_user())
- options, _args = parser.parse_args()
- opts = {}
- for key, val in six.iteritems(options.__dict__):
- opts[key] = val
- return opts
- class Swarm(object):
- """
- Create a swarm of minions
- """
- def __init__(self, opts):
- self.opts = opts
- # If given a temp_dir, use it for temporary files
- if opts["temp_dir"]:
- self.swarm_root = opts["temp_dir"]
- else:
- # If given a root_dir, keep the tmp files there as well
- if opts["root_dir"]:
- tmpdir = os.path.join(opts["root_dir"], "tmp")
- else:
- tmpdir = opts["root_dir"]
- self.swarm_root = tempfile.mkdtemp(
- prefix="mswarm-root", suffix=".d", dir=tmpdir
- )
- if self.opts["transport"] == "zeromq":
- self.pki = self._pki_dir()
- self.zfill = len(str(self.opts["minions"]))
- self.confs = set()
- random.seed(0)
- def _pki_dir(self):
- """
- Create the shared pki directory
- """
- path = os.path.join(self.swarm_root, "pki")
- if not os.path.exists(path):
- os.makedirs(path)
- print("Creating shared pki keys for the swarm on: {0}".format(path))
- subprocess.call(
- "salt-key -c {0} --gen-keys minion --gen-keys-dir {0} "
- "--log-file {1} --user {2}".format(
- path, os.path.join(path, "keys.log"), self.opts["user"],
- ),
- shell=True,
- )
- print("Keys generated")
- return path
- def start(self):
- """
- Start the magic!!
- """
- if self.opts["master_too"]:
- master_swarm = MasterSwarm(self.opts)
- master_swarm.start()
- minions = MinionSwarm(self.opts)
- minions.start_minions()
- print("Starting minions...")
- # self.start_minions()
- print("All {0} minions have started.".format(self.opts["minions"]))
- print("Waiting for CTRL-C to properly shutdown minions...")
- while True:
- try:
- time.sleep(5)
- except KeyboardInterrupt:
- print("\nShutting down minions")
- self.clean_configs()
- break
- def shutdown(self):
- """
- Tear it all down
- """
- print("Killing any remaining running minions")
- subprocess.call('pkill -KILL -f "python.*salt-minion"', shell=True)
- if self.opts["master_too"]:
- print("Killing any remaining masters")
- subprocess.call('pkill -KILL -f "python.*salt-master"', shell=True)
- if not self.opts["no_clean"]:
- print("Remove ALL related temp files/directories")
- shutil.rmtree(self.swarm_root)
- print("Done")
- def clean_configs(self):
- """
- Clean up the config files
- """
- for path in self.confs:
- pidfile = "{0}.pid".format(path)
- try:
- try:
- with salt.utils.files.fopen(pidfile) as fp_:
- pid = int(fp_.read().strip())
- os.kill(pid, signal.SIGTERM)
- except ValueError:
- pass
- if os.path.exists(pidfile):
- os.remove(pidfile)
- if not self.opts["no_clean"]:
- shutil.rmtree(path)
- except (OSError, IOError):
- pass
- class MinionSwarm(Swarm):
- """
- Create minions
- """
- def start_minions(self):
- """
- Iterate over the config files and start up the minions
- """
- self.prep_configs()
- for path in self.confs:
- cmd = "salt-minion -c {0} --pid-file {1}".format(
- path, "{0}.pid".format(path)
- )
- if self.opts["foreground"]:
- cmd += " -l debug &"
- else:
- cmd += " -d &"
- subprocess.call(cmd, shell=True)
- time.sleep(self.opts["start_delay"])
- def mkconf(self, idx):
- """
- Create a config file for a single minion
- """
- data = {}
- if self.opts["config_dir"]:
- spath = os.path.join(self.opts["config_dir"], "minion")
- with salt.utils.files.fopen(spath) as conf:
- data = salt.utils.yaml.safe_load(conf) or {}
- minion_id = "{0}-{1}".format(self.opts["name"], str(idx).zfill(self.zfill))
- dpath = os.path.join(self.swarm_root, minion_id)
- if not os.path.exists(dpath):
- os.makedirs(dpath)
- data.update(
- {
- "id": minion_id,
- "user": self.opts["user"],
- "cachedir": os.path.join(dpath, "cache"),
- "master": self.opts["master"],
- "log_file": os.path.join(dpath, "minion.log"),
- "grains": {},
- }
- )
- if self.opts["transport"] == "zeromq":
- minion_pkidir = os.path.join(dpath, "pki")
- if not os.path.exists(minion_pkidir):
- os.makedirs(minion_pkidir)
- minion_pem = os.path.join(self.pki, "minion.pem")
- minion_pub = os.path.join(self.pki, "minion.pub")
- shutil.copy(minion_pem, minion_pkidir)
- shutil.copy(minion_pub, minion_pkidir)
- data["pki_dir"] = minion_pkidir
- elif self.opts["transport"] == "tcp":
- data["transport"] = "tcp"
- if self.opts["root_dir"]:
- data["root_dir"] = self.opts["root_dir"]
- path = os.path.join(dpath, "minion")
- if self.opts["keep"]:
- keep = self.opts["keep"].split(",")
- modpath = os.path.join(os.path.dirname(salt.__file__), "modules")
- fn_prefixes = (fn_.partition(".")[0] for fn_ in os.listdir(modpath))
- ignore = [fn_prefix for fn_prefix in fn_prefixes if fn_prefix not in keep]
- data["disable_modules"] = ignore
- if self.opts["rand_os"]:
- data["grains"]["os"] = random.choice(OSES)
- if self.opts["rand_ver"]:
- data["grains"]["saltversion"] = random.choice(VERS)
- if self.opts["rand_machine_id"]:
- data["grains"]["machine_id"] = hashlib.md5(minion_id).hexdigest()
- if self.opts["rand_uuid"]:
- data["grains"]["uuid"] = str(uuid.uuid4())
- with salt.utils.files.fopen(path, "w+") as fp_:
- salt.utils.yaml.safe_dump(data, fp_)
- self.confs.add(dpath)
- def prep_configs(self):
- """
- Prepare the confs set
- """
- for idx in range(self.opts["minions"]):
- self.mkconf(idx)
- class MasterSwarm(Swarm):
- """
- Create one or more masters
- """
- def __init__(self, opts):
- super(MasterSwarm, self).__init__(opts)
- self.conf = os.path.join(self.swarm_root, "master")
- def start(self):
- """
- Prep the master start and fire it off
- """
- # sys.stdout for no newline
- sys.stdout.write("Generating master config...")
- self.mkconf()
- print("done")
- sys.stdout.write("Starting master...")
- self.start_master()
- print("done")
- def start_master(self):
- """
- Do the master start
- """
- cmd = "salt-master -c {0} --pid-file {1}".format(
- self.conf, "{0}.pid".format(self.conf)
- )
- if self.opts["foreground"]:
- cmd += " -l debug &"
- else:
- cmd += " -d &"
- subprocess.call(cmd, shell=True)
- def mkconf(self): # pylint: disable=W0221
- """
- Make a master config and write it'
- """
- data = {}
- if self.opts["config_dir"]:
- spath = os.path.join(self.opts["config_dir"], "master")
- with salt.utils.files.fopen(spath) as conf:
- data = salt.utils.yaml.safe_load(conf)
- data.update(
- {
- "log_file": os.path.join(self.conf, "master.log"),
- "open_mode": True, # TODO Pre-seed keys
- }
- )
- os.makedirs(self.conf)
- path = os.path.join(self.conf, "master")
- with salt.utils.files.fopen(path, "w+") as fp_:
- salt.utils.yaml.safe_dump(data, fp_)
- def shutdown(self):
- print("Killing master")
- subprocess.call('pkill -KILL -f "python.*salt-master"', shell=True)
- print("Master killed")
- # pylint: disable=C0103
- if __name__ == "__main__":
- swarm = Swarm(parse())
- try:
- swarm.start()
- finally:
- swarm.shutdown()
|