# HG changeset patch # User Jeff Hammel # Date 1486343422 0 # Node ID 0f44ee07317316b82ec3dc5d68e3b5fb11658533 fake salt, initial commit diff -r 000000000000 -r 0f44ee073173 README.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README.md Mon Feb 06 01:10:22 2017 +0000 @@ -0,0 +1,27 @@ +# KCl + +fake salt + + + .-"""""-. + |'-----'| + |'-.....-'| + | | + | K Cl | + | | + | | + |-.......-| + '-.......-' + + + +When you have salt...use it, of course! When you don't, there's this: +https://en.wikipedia.org/wiki/Potassium_chloride , or +https://en.wikipedia.org/wiki/Salt_substitute . + +---- + +Jeff Hammel + + + diff -r 000000000000 -r 0f44ee073173 kcl/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/kcl/__init__.py Mon Feb 06 01:10:22 2017 +0000 @@ -0,0 +1,2 @@ +# + diff -r 000000000000 -r 0f44ee073173 kcl/ec2.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/kcl/ec2.py Mon Feb 06 01:10:22 2017 +0000 @@ -0,0 +1,526 @@ +""" +code for interfacing with EC2 instances: + + curl http://169.254.169.254/latest/meta-data/ +""" + +# imports +import argparse +import boto.utils +import hashlib +import hmac +import json +import os +import requests +import sys +import urllib +import urlparse +import ConfigParser +from collections import OrderedDict +from datetime import datetime + + +class FilesystemCredentials(object): + """ + Read credentials from the filesystem. See: + - http://boto.cloudhackers.com/en/latest/boto_config_tut.html + - https://blogs.aws.amazon.com/security/post/Tx3D6U6WSFGOK2H/A-New-and-Standardized-Way-to-Manage-Credentials-in-the-AWS-SDKs + + In Unix/Linux systems, on startup, the boto library looks + for configuration files in the following locations + and in the following order: + + /etc/boto.cfg - for site-wide settings that all users on this machine will use + (if profile is given) ~/.aws/credentials - for credentials shared between SDKs + (if profile is given) ~/.boto - for user-specific settings + ~/.aws/credentials - for credentials shared between SDKs + ~/.boto - for user-specific settings + + """ + + def read_aws_credentials(self, fp, section='default'): + parser = ConfigParser.RawConfigParser() + parser.readfp(fp) + if section in parser.sections(): + key = 'aws_access_key_id' + if parser.has_option(section, key): + secret = 'aws_secret_access_key' + if parser.has_option(section, secret): + return (parser.get(section, key), + parser.get(section, secret)) + + def __init__(self): + self.resolution = OrderedDict() + home = os.environ['HOME'] + if home: + self.resolution[os.path.join(home, '.aws', 'credentials')] = self.read_aws_credentials + + def __call__(self): + """ + return credentials....*if* available + """ + + for path, method in self.resolution.items(): + if os.path.isfile(path): + with open(path, 'r') as f: + credentials = method(f) + if credentials: + return credentials + + +class EC2Metadata(object): + """EC2 instance metadata interface""" + + def __init__(self, **kwargs): + self._kwargs = kwargs + + def __call__(self): + """http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html""" + return boto.utils.get_instance_metadata(**self._kwargs) + + def security_credentials(self): + """ + return IAM credentials for an instance, if possible + + See: + http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#instance-metadata-security-credentials + """ + # TODO: nested dict -> object notation mapping ; + # note also this is actually a `LazyLoader` value, + # not actually a dict + + return self()['iam']['security-credentials'] + + def credentials(self, role=None): + """return active credentials""" + + security_credentials = self.security_credentials() + if not security_credentials: + raise AssertionError("No security credentials available") + + roles=', '.join(sorted(security_credentials.keys())) + if role is None: + if len(security_credentials) > 1: + raise AssertionError("No role given and multiple roles found for instance: {roles}".format(roles)) + role = security_credentials.keys()[0] + + if role not in security_credentials: + raise KeyError("Role {role} not in available IAM roles: {roles}".format(role=role, roles=roles)) + + return security_credentials[role] + +class AWSCredentials(FilesystemCredentials): + """ + try to read credentials from the filesystem + then from ec2 metadata + """ + + def __call__(self): + + # return filesystem crednetials, if any + credentials = FilesystemCredentials.__call__(self) + if credentials: + return credentials + + # otherwise try to return credentials from metadata + metadata = EC2Metadata() + try: + ec2_credentials = metadata.credentials() + except AssertionError: + return + keys = ('AccessKeyId', 'SecretAccessKey') + if set(keys).issubset(ec2_credentials.keys()): + return [ec2_credentials[key] + for key in keys] + +class SignedRequest(object): + """ + Signed request using Signature Version 4 + + http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html + """ + + # signing information: + algorithm = 'AWS4-HMAC-SHA256' + termination_string = 'aws4_request' + authorization_header = "{algorithm} Credential={access_key}/{credential_scope}, SignedHeaders={signed_headers}, Signature={signature}" + + # date format: + date_format = '%Y%m%d' + time_format = '%H%M%S' + + ### date methods + + @classmethod + def datetime_format(cls): + return '{date_format}T{time_format}Z'.format(date_format=cls.date_format, + time_format=cls.time_format) + + @classmethod + def datetime(cls, _datetime=None): + """ + returns formatted datetime string as appropriate + for `x-amz-date` header + """ + + if _datetime is None: + _datetime = datetime.utcnow() + return _datetime.strftime(cls.datetime_format()) + + ### constructor + + def __init__(self, access_key, secret_key, region, service): + self.access_key = access_key + self.secret_key = secret_key + self.region = region + self.service = service + + ### hashing methods + + def hash(self, message): + """hash a `message`""" + # from e.g. http://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html#sig-v4-examples-get-auth-header + return hashlib.sha256(message).hexdigest() + + def sign(self, key, msg): + """ + See: + http://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html#signature-v4-examples-python + """ + return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest() + + def signature_key(self, date_stamp): + parts = [date_stamp, + self.region, + self.service, + self.termination_string] + signed = ('AWS4' + self.secret_key).encode('utf-8') + while parts: + signed = self.sign(signed, parts.pop(0)) + return signed + + ### + + def credential_scope(self, date_string): + """ + a string that includes the date, the region you are targeting, + the service you are requesting, and a termination string + ("aws4_request") in lowercase characters. + """ + + parts = [date_string, + self.region, + self.service, + self.termination_string] + # TODO: "The region and service name strings must be UTF-8 encoded." + return '/'.join(parts) + + ### method for canonical components + + @classmethod + def canonical_uri(cls, path): + """ + The canonical URI is the URI-encoded version + of the absolute path component of the URI + """ + + if path == '/': + path = None + if path: + canonical_uri = urllib.quote(path, safe='') + else: + # If the absolute path is empty, use a forward slash (/) + canonical_uri = '/' + return canonical_uri + + @classmethod + def canonical_query(cls, query_string): + """ + returns the canonical query string + """ + # TODO: currently this does not use `cls` + + # split into parameter names + values + query = urlparse.parse_qs(query_string) + + # make this into a more appropriate data structure for processing + keyvalues = sum([[[key, value] for value in values] + for key, values in query.items()], []) + + # a. URI-encode each parameter name and value + def encode(string): + return urllib.quote(string, safe='/') + encoded = [[encode(string) for string in pair] + for pair in keyvalues] + + # b. Sort the encoded parameter names by character code in ascending order (ASCII order) + encoded.sort() + + # c. Build the canonical query string by starting with the first parameter name in the sorted list. + # d. For each parameter, append the URI-encoded parameter name, followed by the character '=' (ASCII code 61), followed by the URI-encoded parameter value. + # e. Append the character '&' (ASCII code 38) after each parameter value, except for the last value in the list. + retval = '&'.join(['{name}={value}'.format(name=name, + value=value) + for name, value in encoded]) + return retval + + @classmethod + def signed_headers(cls, headers): + """ + return a list of signed headers + """ + names = [name.lower() for name in headers.keys()] + names.sort() + return ';'.join(names) + + @classmethod + def canonical_headers(cls, headers): + """ + return canonical headers: + Construct each header according to the following rules: + * Append the lowercase header name followed by a colon. + * ... + See: + - http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html + - http://docs.python-requests.org/en/latest/user/quickstart/#custom-headers + """ + + canonical_headers = [] + for key, value in headers.items(): + + # convert header name to lowercase + key = key.lower() + + # trim excess white space from the header values + value = value.strip() + + # convert sequential spaces in the value to a single space. + # However, do not remove extra spaces from any values + # that are inside quotation marks. + quote = '"' + if not (value and value[0] == quote and value[-1] == quote): + value = ' '.join(value.split()) + + canonical_headers.append((key, value)) + + # check for duplicate headers + names = [name for name, value in canonical_headers] + if len(set(names)) != len(names): + raise AssertionError("You have duplicate header names :( While AWS supports this use-case, this library doesn't yet") + # Append a comma-separated list of values for that header. + # If there are duplicate headers, the values are comma-separated. + # Do not sort the values in headers that have multiple values. + + # Build the canonical headers list by sorting the headers by lowercase character code + canonical_headers.sort(key=lambda x: x[0]) + + # return canonical headers + return canonical_headers + + def __call__(self, url, method='GET', headers=None, session=None): + """create a signed request and return the response""" + + if session: + raise NotImplementedError('TODO') + else: + session = requests.Session() + signed_request = self.signed_request(url, + method=method, + headers=headers) + response = session.send(signed_request) + return response + + def canonical_request(self, url, headers, payload='', method='GET'): + """ + Return canonical request + + url: "http://k0s.org/home/documents and settings" + GET + %2Fhome%2Fdocuments%20and%20settings + ... + """ + + # parse the url + parsed = urlparse.urlsplit(url) + + # get canonical URI + canonical_uri = self.canonical_uri(parsed.path) + + # construct the canonical query string + canonical_query = self.canonical_query(parsed.query) + + # get the canonical headers + canonical_headers = self.canonical_headers(headers) + + # format the canonical headers + canonical_header_string = '\n'.join(['{0}:{1}'.format(*header) + for header in canonical_headers]) + '\n' + + # get the signed headers + signed_headers = self.signed_headers(headers) + + # get the hashed payload + hashed_payload = self.hash(payload) + + # join the parts to make the request: + # http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html + # CanonicalRequest = + # HTTPRequestMethod + '\n' + + # CanonicalURI + '\n' + + # CanonicalQueryString + '\n' + + # CanonicalHeaders + '\n' + + # SignedHeaders + '\n' + + # HexEncode(Hash(RequestPayload)) + parts = [method, + canonical_uri, + canonical_query, + canonical_header_string, + signed_headers, + hashed_payload] + canonical_request = '\n'.join(parts) + return canonical_request + + def signed_request(self, url, method='GET', headers=None): + """ + prepare a request: + http://docs.python-requests.org/en/latest/user/advanced/#prepared-requests + """ + + # parse the URL, since we like doing that so much + parsed = urlparse.urlsplit(url) + + # setup the headers + if headers is None: + headers = {} + headers = OrderedDict(headers).copy() + mapping = dict([(key.lower(), key) for key in headers]) + # XXX this is..."fun" + # maybe we should just x-form everything to lowercase now? + # ensure host header is set + if 'host' not in mapping: + headers['Host'] = parsed.netloc + # add the `x-amz-date` in terms of now: + # http://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonRequestHeaders.html + if 'x-amz-date' not in mapping: + headers['x-amz-date'] = self.datetime() + + # create a PreparedRequest + req = requests.Request(method, url, headers) + prepped = req.prepare() + + # return a signed version + return self.sign_request(prepped) + + def sign_request(self, request): + """ + sign a request; + http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html + """ + + # ensure that we have a `x-amz-date` header in the request + request_date = request.headers.get('x-amz-date') + # Food for thought: perhaps here is a more appropriate place + # to add the headers? probably. Likely. + if request_date is None: + raise NotImplementedError('TODO') + + # get the canonical request + canonical_request = self.canonical_request(method=request.method, + url=request.url, + headers=request.headers) + + # Create a digest (hash) of the canonical request + # with the same algorithm that you used to hash the payload. + hashed_request = self.hash(canonical_request) + + # Create the string to sign: + # http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html + # 1. Start with the algorithm designation + parts = [self.algorithm] + # 2. Append the request date value + parts.append(request_date) # XXX we could validate the format + # 3. Append the credential scope value + date_string = request_date.split('T')[0] # XXX could do better + credential_scope = self.credential_scope(date_string) + parts.append(credential_scope) + # 4. Append the hash of the canonical request + parts.append(hashed_request) + string_to_sign = '\n'.join(parts) + + # Calculate the AWS Signature Version 4 + # http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html + # 1. Derive your signing key. + signing_key = self.signature_key(date_string) + # 2. Calculate the signature + signature = hmac.new(signing_key, + string_to_sign.encode('utf-8'), + hashlib.sha256).hexdigest() + + # Add the signing information to the request + # http://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html + authorization = self.authorization_header.format(algorithm=self.algorithm, + access_key=self.access_key, + credential_scope=credential_scope, + signed_headers=self.signed_headers(request.headers), + signature=signature) + request.headers['Authorization'] = authorization + + # return the prepared requests + return request + + +def main(args=sys.argv[1:]): + """CLI""" + + # parse command line + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--credentials', '--print-credentials', + dest='print_credentials', + action='store_true', default=False, + help="print default credentials for instance") + parser.add_argument('--url', dest='url', + help="hit this URL with a signed HTTP GET request") + parser.add_argument('--service', dest='service', default='es', + help="AWS service to use") + parser.add_argument('--region', dest='region', default='us-west-1', + help="AWS region") + # TODO: `service` and `region` come from + # http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html + # We need to be able to derive the region from the environment. + # It would be very nice to derive the service from, say, the host + options = parser.parse_args(args) + + if options.url: + + # get credentials + credentials = AWSCredentials()() + if not credentials: + parser.error("No AWS credentials found") + aws_key, aws_secret = credentials + + # make a signed request to the URL and exit + request = SignedRequest(aws_key, + aws_secret, + region=options.region, + service=options.service) + response = request(options.url, method='GET') + print ('-'*10) + print (response.text) + response.raise_for_status() + return + + # metadata interface + metadata = EC2Metadata() + + # get desired data + if options.print_credentials: + data = metadata.credentials() + else: + data = metadata() + + # display data + print (json.dumps(data, indent=2, sort_keys=True)) + + +if __name__ == '__main__': + main() diff -r 000000000000 -r 0f44ee073173 kcl/kcl.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/kcl/kcl.py Mon Feb 06 01:10:22 2017 +0000 @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +fake salt +""" + +# imports +import argparse +import json +import ssh +import sys + +class KCl(object): + """fake salt's interface to real salt""" + + def __init__(self, salt_master): + self.salt_master = salt_master + self.ssh = ssh.Ssh(self.salt_master) + + def __call__(self, target, module, *args): + command = self.command(target, module, *args) + code, output = self.ssh.sudo(command) + + return code, json.loads(output) + + def command(self, target, module, *args): + retval = "salt --output=json --static '{target}' {module}".format(target=target, module=module) + for arg in args: + retval += " '{arg}'".format(arg=arg) + return retval + + def ping(self, target='*'): + return self(target, 'test.ping') + + def run(self, command, target='*'): + return self(target, 'cmd.run', command) + + +def main(args=sys.argv[1:]): + """CLI""" + + # parse command line + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('salt_master') + options = parser.parse_args(args) + + # instantiate KCl + kcl = KCl(options.salt_master) + + # ping, just for fun + code, pong = kcl.ping() + + # output + print (json.dumps(pong, indent=2, sort_keys=True)) + + sys.exit(code) + +if __name__ == '__main__': + main() diff -r 000000000000 -r 0f44ee073173 kcl/run_cmd.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/kcl/run_cmd.py Mon Feb 06 01:10:22 2017 +0000 @@ -0,0 +1,88 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +run a command on some things +""" + +# imports +import argparse +import os +import subprocess +import sys +import time + +# module globals +__all__ = ['print_command', 'call', 'RunCmdParser'] +string = (str, unicode) + +def print_command(command, **kwargs): + if not isinstance(command, string): + command = subprocess.list2cmdline(command) + print (command) + +def call(command, **kwargs): + + process = subprocess.Popen(command, **kwargs) + stdout, stderr = process.communicate() + if process.returncode: + cmdline = subprocess.list2cmdline(command) + print cmdline + raise subprocess.CalledProcessError(process.returncode, + cmdline) + +def ssh_call(host, command, ssh='ssh'): + call([ssh, host, command], stderr=subprocess.PIPE) + + +class RunCmdParser(argparse.ArgumentParser): + """CLI option parser""" + + def __init__(self, **kwargs): + kwargs.setdefault('formatter_class', argparse.RawTextHelpFormatter) + kwargs.setdefault('description', __doc__) + argparse.ArgumentParser.__init__(self, **kwargs) + self.add_argument('-H', '--host', dest='hosts', nargs='+', + help="hosts to run on; or read from stdin if omitted") + self.add_argument("command", + help="command to run") + self.options = None + self._hosts = None + + def parse_args(self, *args, **kw): + options = argparse.ArgumentParser.parse_args(self, *args, **kw) + self.validate(options) + self.options = options + return options + + def validate(self, options): + """validate options""" + + def hosts(self): + if self._hosts is None: + assert self.options is not None + self._hosts = self.options.hosts or sys.stdin.read().strip().split() + return self._hosts + + def command(self): + return self.options.command + +def main(args=sys.argv[1:]): + """CLI""" + + # parse command line options + parser = RunCmdParser() + options = parser.parse_args(args) + + # get command to run + command = parser.command() + + # call the command on all hosts + for host in parser.hosts(): + ssh_call(host, command) + + +if __name__ == '__main__': + main() + + diff -r 000000000000 -r 0f44ee073173 kcl/ssh.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/kcl/ssh.py Mon Feb 06 01:10:22 2017 +0000 @@ -0,0 +1,62 @@ +""" +crappy python ssh wrapper +""" + +# imports +import subprocess + +string = (str, unicode) + + +def call(command, check=True, echo=True, **kwargs): + + cmd_str = command if isinstance(command, string) else subprocess.list2cmdline(command) + kwargs['shell'] = isinstance(command, string) + kwargs['stdout'] = subprocess.PIPE + kwargs['stderr'] = subprocess.PIPE + if echo: + print (cmd_str) + process = subprocess.Popen(command, **kwargs) + stdout, stderr = process.communicate() + if check and process.returncode: + raise subprocess.CalledProcessError(process.returncode, command, stdout) + return process.returncode, stdout, stderr + + +class Ssh(object): + """primative form of ssh session using subprocess""" + + def __init__(self, host, ssh='ssh', scp='scp', verbose=False): + self.host = host + self._ssh = ssh + self._scp = scp + self.verbose=verbose + + def command(self, command): + + # See: + # http://unix.stackexchange.com/questions/122616/why-do-i-need-a-tty-to-run-sudo-if-i-can-sudo-without-a-password + # http://unix.stackexchange.com/a/122618 + return [self._ssh, '-t', self.host, command] + + def sudo_command(self, command): + return self.command('sudo ' + command) + + def call(self, command): + returncode, output, _ = call(self.command(command), + echo=self.verbose) + return (returncode, output) + + def sudo(self, command): + returncode, output, _ = call(self.sudo_command(command), + echo=self.verbose) + return (returncode, output) + + def scp(self, src, dest): + """scp a file to the given host""" + + command = [self._scp, + src, + '{host}:{dest}'.format(host=self.host, dest=dest)] + returncode, output, _ = call(command) + return (returncode, output) diff -r 000000000000 -r 0f44ee073173 kcl/sudo_scp.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/kcl/sudo_scp.py Mon Feb 06 01:10:22 2017 +0000 @@ -0,0 +1,67 @@ +#!/usr/bin/env python + +""" +scp with sudo and magic +""" + +# imports +import argparse +import os +import subprocess +import sys + +from run_cmd import call +from run_cmd import print_command + + +def main(args=sys.argv[1:]): + """CLI""" + + # parse command line + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('src') + parser.add_argument('dest') + parser.add_argument('host', nargs='*', + help="hosts to copy to, or stdin if omitted") + parser.add_argument('-u', '--user', dest='user', + help="what user should own this file?") + parser.add_argument('--dry-run', dest='dry_run', + action='store_true', default=False, + help="don't actually do anything; just print what would be done") + options = parser.parse_args(args) + + # sanity + if not os.path.exists(options.src): + parser.error("'{}' does not exist".format(options.src)) + + if options.dry_run: + call = print_command + else: + call = globals()['call'] + + basename = os.path.basename(options.src) + + TMPDIR = '/tmp' + + tmpdest = os.path.join(TMPDIR, basename) + + hosts = options.host or sys.stdin.read().strip().split() + + + # copy to all hosts + for host in hosts: + call(['scp', options.src, '{host}:{dest}'.format(host=host, dest=tmpdest)], + stderr=subprocess.PIPE) + call(['ssh', host, "sudo mv {tmpdest} {dest}".format(tmpdest=tmpdest, dest=options.dest)], + stderr=subprocess.PIPE) + if options.user: + call(['ssh', host, "sudo chown {user} {dest}".format(user=options.user, dest=options.dest)], + stderr=subprocess.PIPE) + call(['ssh', host, "sudo chmod a+x {dest}".format(dest=options.dest)], + stderr=subprocess.PIPE) + call(['ssh', host, "sudo chmod a+r {dest}".format(dest=options.dest)], + stderr=subprocess.PIPE) + + +if __name__ == '__main__': + main() diff -r 000000000000 -r 0f44ee073173 kcl/versions.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/kcl/versions.py Mon Feb 06 01:10:22 2017 +0000 @@ -0,0 +1,51 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +compare package versions across nodes: + +$ sudo rpm -q --qf "%{VERSION}" python-requests +2.6.0 +""" + + +# imports +import argparse +import json +import kcl +import sys + + +def main(args=sys.argv[1:]): + """CLI""" + + # parse command line + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('salt_master') + parser.add_argument('package') + parser.add_argument('--group', dest='group', + action='store_true', default=False, + help="group hosts by versions") + options = parser.parse_args(args) + + # instantiate API + salt = kcl.KCl(options.salt_master) + + # get versions + command = 'rpm -q --qf "%{VERSION}" ' + options.package + code, versions = salt.run(command) + + # output + if options.group: + # group host by package versions + group = {} + for host, version in versions.items(): + group.setdefault(version, []).append(host) + print (json.dumps(group, indent=2, sort_keys=True)) + else: + print (json.dumps(versions, indent=2, sort_keys=True)) + + sys.exit(code) + +if __name__ == '__main__': + main() diff -r 000000000000 -r 0f44ee073173 setup.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/setup.py Mon Feb 06 01:10:22 2017 +0000 @@ -0,0 +1,46 @@ +""" +setup packaging script for KCl +""" + +import os + +version = "0.0" +dependencies = [] + +# allow use of setuptools/distribute or distutils +kw = {} +try: + from setuptools import setup + kw['entry_points'] = """ + [console_scripts] + kcl = kcl.kcl:main + package-versions = kcl.versions:main + run_cmd = kcl.run_cmd:main + sudo_scp = kcl.sudo_scp:main +""" + kw['install_requires'] = dependencies +except ImportError: + from distutils.core import setup + kw['requires'] = dependencies + +try: + here = os.path.dirname(os.path.abspath(__file__)) + description = file(os.path.join(here, 'README.txt')).read() +except IOError: + description = '' + + +setup(name='KCl', + version=version, + description="fake salt", + long_description=description, + classifiers=[], # Get strings from http://www.python.org/pypi?%3Aaction=list_classifiers + author='Jeff Hammel', + author_email='k0scist@gmail.com', + url='', + license='', + packages=['kcl'], + include_package_data=True, + zip_safe=False, + **kw + )