diff --git a/bin/bastion.py b/bin/bastion.py index c4644e1..46cfa58 100755 --- a/bin/bastion.py +++ b/bin/bastion.py @@ -29,7 +29,7 @@ from Bastion.Condo import * from Bastion.Actions import * from Bastion.Model import ARK -from Bastion.CARP import SUCCESS, FAILURE, CRASH, SUCCEEDED, FAILED, CRASHED +from Bastion.CARP import Request import Bastion.Vaults.HPSS import Bastion.Vaults.BFD @@ -134,6 +134,9 @@ def sites(self): yield self.site(nick) def run(self): + #-- generate a session ID. + self.session = "{}{}".format(Quantim.now().quaver, Boggle(5)) + #-- scan the command line for options of the form "-{opt}:{value}" #-- options are removed from the command sequence opts = { } @@ -190,24 +193,20 @@ def run(self): tokens = idiom.split() if tokens == comargs[:len(tokens)]: action = method - request = CARP.Request(idiom, comargs[len(tokens):]) + request = Request(idiom, comargs[len(tokens):], ID = self.session) #-- execute the action within crash guardrails try: - answer = action(comargs, comdex, opts) + answer = action(request) except Exception as err: tb = traceback.format_exception(err) answer = CRASHED( ''.join(tb), tb ) #-- always log crashes! answer.context['log.scope'] = '*' - #-- embed the process tracking info as part of the reply context - for k, v in proc.items(): - answer['context'][k] = v - #-- check for override on the logging scope. if "log.scope" in opts: - answer['context']['log.scope'] = opts['log.scope'] + answer.context['log.scope'] = opts['log.scope'] #-- write the answer to the log, if there is a given log.scope self.record(answer, opts) @@ -218,11 +217,9 @@ def run(self): #-- explicitly exit based on the answer to the request #-- status codes follow the general "theory of reply codes" #-- e.g. those used in SMTP, HTTP, etc. - if answer['reply']['status'][0] in ('1','2','3'): - #-- codes in 100, 200, 300 blocks indicate success + if not answer.status.indicates_failure: sys.exit(0) else: - #-- codes in 400, 500 (others?) indicate failure sys.exit(1) def record(self, answer, opts): @@ -232,13 +229,13 @@ def record(self, answer, opts): I need answer to include "log.scope" in the context block. If no "log.scope" is declared, I silently ignore the request to record. """ - if 'log.scope' in answer['context']: + if 'log.scope' in answer.context: #-- only write a log file if we have an explicit log.scope in the answer's context block. - session = answer['context']['task.session'] + session = answer.request.ID scope = self.logroot - if answer['context']['log.scope'] != '*': - scope = scope / answer['context']['log.scope'] + if answer.context['log.scope'] != '*': + scope = scope / answer.context['log.scope'] halo = scope / "{}.yaml".format(session) halo.parent.mkdir(parents = True, exist_ok = True) @@ -259,23 +256,26 @@ def emit(self, answer, opts, ostream = None): def emit_YAML(self, answer, opts, ostream): yaml = YAML() yaml.default_flow_style = False - answer['report'] = PreservedScalarString(answer['report']) + answer.report = PreservedScalarString(answer.report']) yaml.dump(answer, ostream) def emit_JSON(self, answer, opts, ostream): json.dump(answer, ostream, sort_keys = True, indent = 3) def emit_PROSE(self, answer, opts, ostream): - ostream.write("request: {request}\n".format(**answer)) - ostream.write("reply: {status} {message}\n".format(**answer['reply'])) + request = answer.request + requested = ' '.join([request.action] + request.args) + ostream.write("request: {}\n".format(requested)) + ostream.write("reply: {reply.code} {reply.gloss}\n".format(reply=answer.status)) + ostream.write("# {reply.lede}".format(reply=answer)) ostream.write("----\n") - ostream.write(answer['report']) + ostream.write(answer.report) ostream.write("\n") #---------------------- #-- basic operations | #---------------------- - def do_help(self, comargs, comdex, opts): + def do_help(self, request): #-- generate a document by scanning all of my "do_*" methods. menu = [ ] for attr in dir(self): @@ -303,20 +303,20 @@ def do_help(self, comargs, comdex, opts): doc = '\n----\n'.join(stanzas) - return SUCCESS(doc) + return request.succeeded(doc) #-------------------------------------------------------- #-- BEGIN bank (backup) operations | #-- all "bank" operations create full (level 0) backups | #↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ - def do_bank_site(self, comargs, comdex, opts): + def do_bank_site(self, request): """ bank site {site} * creates a full backup for each asset in site. """ raise NotImplementedError - def do_bank_zone(self, comargs, comdex, opts): + def do_bank_zone(self, request): """ bank zone {site} {zone} bank zone {ARK} @@ -326,25 +326,27 @@ def do_bank_zone(self, comargs, comdex, opts): """ raise NotImplementedError - def do_bank_asset(self, comargs, comdex, opts): + def do_bank_asset(self, request): """ backup asset {ARK} * creates a full backup of {ARK} """ - ark = ARK(comdex[2]) - site = self.site(ark.site) - asset = site.asset(ark) - vault = self.vault(asset.policy.vault) - blonde = vault.push(asset, client = self.hostname) + ark = ARK(request.args[0]) + site = self.site(ark.site) + asset = site.asset(ark) + vault = self.vault(asset.policy.vault) + + receipt = vault.push(asset, client = self.hostname) extras = { 'log.scope': "site/{}".format(ark.site) } - if blonde: - return SUCCESS("pushed full backup {}".format(str(blonde)), context = extras) + if receipt.indicates_success: + blonde = receipt.body['blonde'] + return request.succeeded("pushed full backup of {} to {}".format(str(ark), str(blonde)), receipt, context = extras) else: - return FAILED("something went wrong!", context = extras) + return request.failed("while pushing full backup of {}, something went wrong!".format(str(ark)), receipt, context = extras) #↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ #-- END bank (backup) operations | @@ -354,21 +356,26 @@ def do_bank_asset(self, comargs, comdex, opts): #-- BEGIN amend (differential) operations | #-- all "amend" operations create differential (level 1) backups | #↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ - def do_amend_asset(self, comargs, comdex, opts): + def do_amend_asset(self, request): """ amend asset {ARK} * creates a differential backup of {ARK} """ - ark = ARK(comdex[2]) - site = self.site(ark.site) - asset = site.asset(ark) - vault = self.vault(asset.policy.vault) - flag, stdout, stderr = vault.push(asset, detail = 'D', client = self.hostname) - flag, stdout, stderr = vault.push(asset, detail = 'D') - if flag: - return SUCCESS(stdout, {'stdout': stdout, 'stderr': stderr}) + ark = ARK(request.args[0]) + site = self.site(ark.site) + asset = site.asset(ark) + vault = self.vault(asset.policy.vault) + receipt = vault.push(asset, detail = 'D') + + extras = { + 'log.scope': "site/{}".format(ark.site) + } + + if receipt.indicates_success: + blonde = receipt.body['blonde'] + return request.succeeded("amended asset {} to {}".format(str(ark), str(blonde)), receipt, context = extras) else: - return FAILED(stdout, {'stdout': stdout, 'stderr': stderr}) + return request.failed("amend operation on asset {} failed".format(str(ark)), receipt, context = extras) #↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ #-- END amend (differential) operations | @@ -395,7 +402,7 @@ def do_update_zone(self, comargs, comdex, opts): """ raise NotImplementedError - def do_update_asset(self, comargs, comdex, opts): + def do_update_asset(self, request): """ update asset {ARK} * asset given in ARK format @@ -403,22 +410,25 @@ def do_update_asset(self, comargs, comdex, opts): * performs backup based on determined level * typically used to "automatically" do updates in a scheduled (cron) job """ - ark = ARK(comdex[2]) + ark = ARK(request.args[0]) asset = self.asset(ark) vault = self.vault(asset.policy.vault) + banked = vault.manifest(ark) if banked.anchors: anchor = banked.anchors[-1] - blonde = vault.push(asset, anchor) + receipt = vault.push(asset, anchor) else: - blonde = vault.push(asset) + receipt = vault.push(asset) extras = { 'log.scope': "site/{}".format(ark.site) } - return SUCCESS("pushed update {}".format(str(blonde)), context = extras) - + if receipt.indicates_success: + return request.succeeded("updated asset {}".format(str(ark)), receipt, context = extras) + else: + return request.failed("update request on asset {} failed".format(str(ark)), receipt, context = extras) #↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ #-- END update (automatic) operations | @@ -481,18 +491,19 @@ def do_export_manifest(self, comargs, comdex, opts): return SUCCESS(report, manifest.toJDN()) - def do_export_sites_provisioned(self, comargs, comdex, opts): + def do_export_sites_provisioned(self, request, comargs, comdex, opts): """ export sites provisioned {vault} * lists all sites that are provisioned in the given vault """ - vault = self.vault(comdex[3]) + comdex = dict(enumerate(request.args.keys())) + vault = self.vault(comdex[0]) sites = list(vault.sites) - spool = ["# sites banked in {}".format(vault.name)] + page = ["# sites banked in {}".format(vault.name)] for site in sites: - spool.append("* {}".format(site)) - report = '\n'.join(spool) + page.append("* {}".format(site)) + report = '\n'.join(page) data = {vault.name: sites} return SUCCESS(report, data) diff --git a/lib/Bastion/CARP.py b/lib/Bastion/CARP.py index 11b5b58..bfebf65 100644 --- a/lib/Bastion/CARP.py +++ b/lib/Bastion/CARP.py @@ -111,27 +111,35 @@ def is_inconclusive(self): class isRequest: def __init__(self, action, *args, **kwargs): - self.ID = kwargs.get('ID', str(uuid.uuid4())) - self.action = action - self.args = args - self.when = kwargs.get('when', datetime.datetime.now()) + self.ID = kwargs.get('ID', str(uuid.uuid4())) + self.action = action + self.args = list(args) + self.when = kwargs.get('when', datetime.datetime.now()) + self.argdex = dict(enumerate(args)) + self.context = { } + + if 'context' in kwargs: + for k, v in kwargs['context'].items(): + self.context[k] = v def toJDN(self, **kwargs): jdn = { 'ID': self.ID, 'action': self.action, 'args': list(self.args), - 'opened': self.when.isoformat() + 'opened': self.when.isoformat(), + 'context': { } } - return jdn + for k, v in self.context.items(): + jdn['context'][k] = v + + return jdn -class Request(isRequest): - pass class isReceipt: - def __init__(self, request, status, brief, obj, context = None, **kwargs): + def __init__(self, request, status, obj, context = None, **kwargs): #-- request is a 2-tuple of (action, [arg1, ...]) #-- status is an instance of ReplyStatus #-- brief is an arbitrary (assumed human readable) text string @@ -145,7 +153,7 @@ def __init__(self, request, status, brief, obj, context = None, **kwargs): self.request = request self.status = status - self.brief = str(msg) + self.lede = kwargs.get('lede', status.category) self.body = obj self.context = { } self.when = kwargs.get('when', datetime.datetime.now()) @@ -160,25 +168,27 @@ def toJDN(self, **kwargs): #-- context is an arbitrary set of key-value pairs #-- data is a JSON serializable object that is the body of the receipt. if callable(getattr(obj, 'toJDN', None)): - payload = obj.toJDN() + payload = self.body.toJDN() else: - payload = obj + payload = self.body + + #-- Build result stanza. + result = { + 'status': self.status.code, + 'message': self.status.gloss, + 'lede': self.lede, + 'answered': self.when.isoformat(), + 'context': {}, + 'body': payload + } + #-- Populate the result's context (if any) + for k, v in self.context.items(): + result['context'][k] = v + #-- Construct the JDN for return jdn = { } - jdn['request'] = self.request.toJDN(**kwargs) - - jdn['request']['result'] = { - 'status': self.status.code, - 'message': self.status.gloss, - 'summary': self.brief, - 'closed': self.when.isoformat() - } - - jdn['request']['result']['body'] = { - 'data': payload - } - + jdn['request']['result'] = result return jdn @property @@ -199,25 +209,22 @@ class Receipt(isReceipt): class Report(isReceipt): - def __init__(self, status, brief, doc, obj = None, **kwargs): - isReceipt.__init__(self, status, brief, obj, **kwargs) + def __init__(self, request, status, doc, obj = None, **kwargs): + isReceipt.__init__(self, request, status, obj, **kwargs) self.report = doc def toJDN(self, **kwargs): jdn = isReceipt.toJDN(self, **kwargs) - jdn['request']['result']['body']['report'] = str(self.report) + jdn['request']['result']['report'] = str(self.report) return jdn -def SUCCEEDED(doc, obj = None, **kwargs): - return Report(ReplyStatus.Ok, doc, obj, **kwargs) - -def FAILED(doc, obj = None, **kwargs): - return CARP(ReplyStatus.Failed, doc, obj, **kwargs) +class Request(isRequest): + def succeeded(self, doc, data = None, **kwargs): + return Report(self, ReplyStatus.Ok, doc, data, **kwargs) -def CRASHED(doc, obj = None, **kwargs): - return CARP(ReplyStatus.Crashed, doc, obj, **kwargs) + def failed(self, doc, data = None, **kwargs): + return Report(self, ReplyStatus.Failed, lede, doc, data, **kwargs)) -SUCCESS = SUCCEEDED -FAILURE = FAILED -CRASH = CRASHED + def crashed(self, doc, data = None, **kwargs): + return Report(self, ReplyStatus.Crashed, lede, doc, data, **kwargs))