#!/usr/bin/env python # Copyright (c) 2016 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from __future__ import print_function import datetime import hashlib import pipes import re import subprocess import sys import time try: import jwt except ImportError: print("jwt module not found, Heketi JWT auth won't work.\n" "You can install it with 'pip install PyJWT'.\n", file=sys.stderr) import requests class Objlog(object): def __init__(self, obj): self.object = obj self.__log__ = [] def __getattr__(self, att): av = getattr(self.object, att) if not hasattr(av, '__call__'): return av def alog(*a, **kw): try: ret = av(*a, **kw) except Exception as x: self.__log__.append(((att, a, kw), None, x)) raise x self.__log__.append(((att, a, kw), ret)) return ret return alog def __reset__(self): self.__log__ = [] def objlog_get(ol): return ol.__log__ def reqlog(req, lower=0, upper=None): ol = objlog_get(req) if upper is None: upper = len(ol) for i in range(lower, upper): e = ol[i] print(i, e[0]) print(e[1], e[1].headers, e[1].text) class HeketiException(Exception): pass def jwt_token(method, path, key, issuer="admin", delta={'minutes': 5}): method = method.upper() path = re.sub("\A/+", "/", "/" + path) claims = {} # Issuer claims['iss'] = issuer # Issued at time claims['iat'] = datetime.datetime.utcnow() # Expiration time claims['exp'] = (datetime.datetime.utcnow() + datetime.timedelta(**delta)) # URI tampering protection claims['qsh'] = hashlib.sha256(method + '&' + path).hexdigest() return jwt.encode(claims, key, algorithm='HS256') class HeketiClient(object): """A client class for the Heteki GlusterFS management service.""" def __init__(self, host, requests_like=requests, jwt_key=None): host_stripped = re.sub("/+\Z", "", host) self.__host__ = host_stripped self.__requests__ = requests_like self.__jwt_key__ = jwt_key def __getattr__(self, attr): attr_value = getattr(self.__requests__, attr) if not hasattr(attr_value, '__call__'): return attr_value def _attr_value_host_injected(*a, **kw): if len(a) == 0: return attr_value(*a, **kw) else: path = a[0] path_stripped = re.sub("\A/+", "", path) req_url = "/".join((self.__host__, path_stripped)) if self.__jwt_key__: token = jwt_token(attr, path, self.__jwt_key__) kw['headers'] = {"Authorization": "bearer %s" % token} report("Heketi request %(method)s to %(url)s" % { 'method': attr.upper(), 'url': req_url}) resp = attr_value(req_url, *a[1:], **kw) report("Heketi response: %s" % repr(resp)) return resp return _attr_value_host_injected def asyncop(self, *a, **kw): method = kw.pop('method', 'post') retry_interval = kw.pop('retry_interval', 1) resp = getattr(self, method)(*a, **kw) if resp.status_code != 202: resp.raise_for_status() raise HeketiException(( 'Unexpected Heketi async %(method)s status %(status)d' ) % {'method': method.upper(), 'status': resp.status_code}) queue = resp.headers['location'] while True: resp = self.get(queue) if resp.status_code == 204: return resp elif resp.status_code == 303: return self.get(resp.headers['location']) elif resp.status_code == 200: if 'x-pending' not in resp.headers: return resp else: resp.raise_for_status() raise HeketiException(( 'Unexpected Heketi async queue status %d' ) % resp.status_code) time.sleep(retry_interval) class ShExec(object): def __init__(self, host, user=None, key=None, root=False): self.host = host self.root = root self.args = [] if host == "localhost": self.args = ["sh", "-c"] else: self.args = ["ssh", host] if user: self.args.extend(["-l", user]) if key: self.args.extend(["-i", key]) def __call__(self, cmd): if not self.root: cmd = "sudo sh -c " + pipes.quote(cmd) report("Running on %(host)s: %(cmd)s" % { 'cmd': cmd, 'host': self.host}) po = subprocess.Popen(self.args + [cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = po.communicate() report("Command output: %s" % out, cond=out) if po.returncode: raise RuntimeError("%(cmd)s has failed with %(exit)d" % { 'cmd': ' '.join(self.args) + ' ' + cmd, 'exit': po.returncode}) return out, err def setup(clusters, args, h): cluster = args.cluster if not cluster and clusters: cluster = clusters[0] if cluster not in clusters: if re.match('[\da-f]{8}(-?[\da-f]{4}){3}-?[\da-f]{12}\Z', cluster or '', re.I): raise HeketiException( "Cluster %s not found on Heketi server." % cluster) cluster = None if not cluster: cluster = h.post("clusters").json()['id'] report("Using cluster %s" % cluster) hostdevices = {} for host in args.host: hostdevices[host] = [] shx = ShExec(host, user=args.user, key=args.key, root=args.root) for i in range(args.devices): dev, _ = shx("i=0; while [ -f /LOOP%(cluster)s-$i ]; do i=$(($i+1)); done && " "truncate -s %(size)s /LOOP%(cluster)s-$i && " "losetup -f --show /LOOP%(cluster)s-$i" % {'size': args.size, 'cluster': cluster}) hostdevices[host].append(dev.strip()) for host, devices in hostdevices.items(): # add a node node = h.asyncop("nodes", json={ "zone": 1, "hostnames": {"manage": [host], "storage": [host]}, "cluster": cluster}).json() for dev in devices: # add a device h.asyncop("devices", json={"node": node['id'], "name": dev}) def teardown(cliusters, args, h): if args.cluster not in clusters: raise HeketiException( "Cluster %s not found on Heketi server." % args.cluster) cluster = h.get("clusters/%s" % args.cluster).json() for vol in cluster["volumes"]: h.asyncop('volumes/%s' % vol, method='delete') for nodeid in cluster["nodes"]: node = h.get("nodes/%s" % nodeid).json() for dev in node["devices"]: h.asyncop('devices/%s' % dev['id'], method='delete') h.asyncop('nodes/%s' % nodeid, method='delete') h.delete("clusters/%s" % args.cluster) if __name__ == '__main__': import argparse parser = argparse.ArgumentParser() parser.add_argument("host", nargs='+') parser.add_argument("-v", "--verbose", action='store_true') parser.add_argument("-H", "--heketi", default="http://localhost:8080", help="Heketi service URL") parser.add_argument("-s", "--size", help="size of devices created") parser.add_argument("-n", "--devices", help="number of devices created per node", type=int) parser.add_argument("-c", "--cluster", help="Heketi cluster to use") parser.add_argument("-j", "--jwt", help="JWT key to use with Heketi auth") parser.add_argument("-u", "--user", default="heketi", help="user with which nodes are managed") parser.add_argument("-k", "--key", help="SSH key used to log in remotely") parser.add_argument("--root", action='store_true', help="remote user has root privileges") parser.add_argument("-A", "--action", choices=('setup', 'teardown'), default='setup') parser.add_argument("-D", "--debug", action='store_true') args = parser.parse_args() if args.jwt: jwt if args.verbose: def report(*a, **kw): if not kw.pop('cond', True): return print(*a, **kw) else: report = lambda *a, **kw: None req = requests.session() if args.debug: req = Objlog(req) heketi = HeketiClient(args.heketi, requests_like=req, jwt_key=args.jwt) try: # get a cluster clusters = heketi.get("clusters").json()['clusters'] getattr(sys.modules[__name__], args.action)(clusters, args, heketi) finally: if args.debug: reqlog(req)