arm-sdk/arm/zuper

858 lines
19 KiB
Bash
Executable File

#!/usr/bin/env zsh
## -*- origami-fold-style: triple-braces -*-
#
# Zuper - Zsh Ultimate Programmer's Extensions Refurbished
#
# Copyright (C) 2015 Dyne.org Foundation
#
# Zuper is designed, written and maintained by Denis Roio <jaromil@dyne.org>
#
# This source code is free software; you can redistribute it and/or
# modify it under the terms of the GNU Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This source code is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# Please refer to the GNU Public License for more details.
#
# You should have received a copy of the GNU Public License along with
# this source code; if not, write to:
# Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
##########################
typeset -aU vars
typeset -aU arrs
typeset -aU maps
vars=(DEBUG QUIET LOG)
arrs=(req freq)
vars+=(zuper_version)
zuper_version=0.2
zmodload zsh/system
zmodload zsh/net/tcp
# {{{ Messaging
# Messaging function with pretty coloring
autoload colors
colors
vars+=(last_act last_func last_notice)
function _msg() {
local msg="$2"
command -v gettext 1>/dev/null 2>/dev/null && msg="$(gettext -s "$2")"
for i in $(seq 3 ${#});
do
msg=${(S)msg//::$(($i - 2))*::/$*[$i]}
done
local command="print -P"
local progname="$fg[magenta]${PROGRAM##*/}$reset_color"
local message="$fg_bold[normal]$fg_no_bold[normal]$msg$reset_color"
local -i returncode
case "$1" in
inline)
command+=" -n"; pchars=" > "; pcolor="yellow"
;;
message)
last_act="$msg"
pchars=" . "; pcolor="white"; message="$fg_no_bold[$pcolor]$msg$reset_color"
;;
verbose)
last_func="$msg"
pchars="[D]"; pcolor="blue"
;;
success)
last_notice="$msg"
pchars="(*)"; pcolor="green"; message="$fg_no_bold[$pcolor]$msg$reset_color"
;;
warning)
pchars="[W]"; pcolor="yellow"; message="$fg_no_bold[$pcolor]$msg$reset_color"
;;
failure)
pchars="[E]"; pcolor="red"; message="$fg_no_bold[$pcolor]$msg$reset_color"
returncode=1
;;
print)
progname=""
;;
*)
pchars="[F]"; pcolor="red"
message="Developer oops! Usage: _msg MESSAGE_TYPE \"MESSAGE_CONTENT\""
returncode=127
zerr
;;
esac
${=command} "${progname} $fg_bold[$pcolor]$pchars$reset_color ${message}$color[reset_color]" >&2
# write the log if its configured
[[ "$LOG" = "" ]] || {
touch $LOG || return $?
${=command} "${progname} $fg_bold[$pcolor]$pchars$reset_color ${message}$color[reset_color]" >> $LOG
}
return $returncode
}
function _message say act() {
local notice="message"
[[ "$1" = "-n" ]] && shift && notice="inline"
[[ $QUIET = 1 ]] || _msg "$notice" $@
return 0
}
function _verbose xxx func() {
[[ $DEBUG = 1 ]] && _msg verbose $@
return 0
}
function _success yes notice() {
[[ $QUIET = 1 ]] || _msg success $@
return 0
}
function _warning no warn warning() {
[[ $QUIET = 1 ]] || _msg warning $@
return 0
}
function _failure fatal die error() {
# typeset -i exitcode=${exitv:-1}
[[ $QUIET = 1 ]] || _msg failure $@
return 1
}
function _print() {
[[ $QUIET = 1 ]] || _msg print $@
return 0
}
# }}} Messaging
# {{{ Debugging
fn() {
fun="$@"
req=()
freq=()
func "$fun"
}
zerr() {
error "error in: ${fun:-$last_notice}"
[[ "$last_func" = "" ]] || warn "called in: $last_func"
[[ "$last_act" = "" ]] || warn "called in: $last_act"
[[ "$last_notice" = "" ]] || warn "called in: $last_notice"
# [[ "$fun" = "" ]] || warn "called in: $fun"
TRAPEXIT() {
error "error reported, operation aborted."
}
return 1
}
ckreq reqck() {
err=0
for v in $req; do
[[ "${(P)v}" = "" ]] && {
warn "${fun[(ws: :)1]}(): required setting is blank: $v"
err=1
}
done
[[ $err = 1 ]] && return $err
for f in $freq; do
# exists and has size greater than zero
[[ -s $f ]] || {
warn "required file empty: $f"
err=1
}
done
[[ $err == 1 ]] && zerr
return $err
}
zdump() {
fn zdump
[[ ${#vars} -gt 0 ]] && {
print "Global variables:"
for _v in $vars; do
print " $_v = \t ${(P)_v}"
done
}
[[ ${#arrs} -gt 0 ]] && {
print "Global arrays:"
for _a in $arrs; do
print " $_a \t ( ${(P)_a} )"
done
}
[[ ${#maps} -gt 0 ]] && {
print "Global maps:"
for _m in $maps; do
print " $_m [key] \t ( ${(Pk)_m} )"
print " $_m [val] \t ( ${(Pv)_m} )"
done
}
}
# handy wrappers for throw/catch execution of blocks where we need the
# program to exit on any error (non-zero) returned by any function
throw() { function TRAPZERR() { zerr; return 1 } }
catch() { function TRAPZERR() { } }
##########################
# Endgame handling
arrs+=(destruens)
destruens=()
# Trap functions for the endgame event
TRAPINT() { endgame INT; return $? }
# TRAPEXIT() { endgame EXIT; return $? }
TRAPHUP() { endgame HUP; return $? }
TRAPQUIT() { endgame QUIT; return $? }
TRAPABRT() { endgame ABORT; return $? }
TRAPKILL() { endgame KILL; return $? }
# TRAPPIPE() { endgame PIPE; return $? }
TRAPTERM() { endgame TERM; return $? }
TRAPSTOP() { endgame STOP; return $? }
# TRAPZERR() { func "function returns non-zero." }
endgame() {
fn "endgame $*"
# execute all no matter what
TRAPZERR() { }
# process registered destructors
for d in $destruens; do
fn "destructor: $d"
$d
done
return 0
}
# Register endgame() to be called at exit.
# unlike TRAPEXIT, the zshexit() hook is not called when functions exit.
zshexit() { endgame EXIT; return $? }
# }}} Debugging
# {{{ Tempfiles
##########################
# Temp file handling
vars+=(ztmpfile)
# ztmp() fills in $ztmpfile global. Caller must copy that variable as
# it will be overwritten at every call.
ztmp() {
fn ztmp
ztmpfile=`mktemp`
tmpfiles+=($ztmpfile)
}
# All tempfiles are freed in endgame()
_ztmp_destructor() {
fn _ztmp_destructor
for f in $tmpfiles; do
rm -f "$f"
done
tmpfiles=()
}
arrs+=(tmpfiles)
destruens+=(_ztmp_destructor)
# }}} Tempfiles
# {{{ Strings
# tokenizer, works only with one char length delimiters
# saves everything in global array tok=()
arrs+=(tok)
strtok() {
fn "strtok $*"
_string="$1"
_delim="$2"
req=(_string _delim)
ckreq || return $?
tok=()
f=0
c=0
for c in {1..${#_string}}; do
if [[ "${_string[(e)$c]}" == "$_delim" ]]; then
# check if not empty
t=${_string[(e)$(($f + 1)),$(($c - 1))]}
[[ "$t" == "" ]] || tok+=($t)
# save last found
f=$c
fi
done
# add last token
t=${_string[(e)$(($f + 1)),$c]}
[[ "$t" == "" ]] || tok+=($t)
}
# TODO: move in here some helpers
# }}} Strings
# {{{ Networking
# This is only tested on GNU/Linux and makes use of sysfs
# index of all network devices
arrs+=(net_devices)
# map of ipv4 assigned addresses: [dev addr]
maps+=(net_ip4_addr)
# map of ipv6 assigned addresses: [dev addr]
maps+=(net_ip6_addr)
# map of dhcp served ipv4
maps+=(ip4dhcps)
# map of dhcp served ipv6
maps+=(ip6dhcps)
# map of external ipv4 addresses
maps+=(net_ip4_exit)
# map of internal ipv6 addresses
# maps+=(ip6exits)
net.scan_devices() {
for i in `find /sys/devices/ -name net`; do
dev=`ls $i`
# skip the loopback device
[[ "$dev" = "lo" ]] && continue
net_devices+=($dev)
done
# return error if no device found
if [[ ${#net_devices} = 0 ]]; then return 1
else return 0; fi
}
net.scan_addresses() {
[[ ${#net_devices} = 0 ]] && {
error "No network device found."
func "Have you ran net.scan_devices() first?"
return 1
}
for dev in ${net_devices}; do
# check ipv4 connections
conn=`ip addr show $dev | awk '/inet / {print $2}'`
[[ "$conn" = "" ]] || {
net_ip4_addr+=($dev $conn) }
# check ipv6 connections
conn=`ip addr show $dev | awk '/inet6/ {print $2}'`
[[ "$conn" = "" ]] || {
net_ip6_addr+=($dev $conn) }
done
# list ipv4
notice "${#net_ip4_addr} ipv4 connected devices found"
for c in ${(k)net_ip4_addr}; do
act " $c ${net_ip4_addr[$c]}"
done
# list ipv6
notice "${#net_ip6_addr} ipv6 connected devices found"
for c in ${(k)net_ip6_addr}; do
act " $c ${net_ip6_addr[$c]}"
done
# find out network addresses
return 0
}
net.scan_exits() {
# just ipv4 for now, also we use curl to drive the call over the
# specific interface, but if that wouldn't matter then rest.get is
# better to avoid this dependency
for dev in ${(k)net_ip4_addr}; do
addr=`curl --silent --interface $dev https://api.ipify.org`
if [[ "$?" != "0" ]]; then
error "curl returns $?: $addr"
else
[[ "$addr" = "" ]] || {
notice "$dev external ip: $addr"
net_ip4_exit+=($dev $addr)
}
fi
done
for dev in ${(k)net_ip6_addr}; do
addr=`curl --silent --ipv6 --interface $dev https://api.ipify.org`
if [[ $? != 0 ]]; then
error "curl returns $?: $addr"
else
[[ "$addr" = "" ]] || {
notice "$dev external ip: $addr"
net_ip4_exit+=($dev $addr)
}
fi
done
}
# }}} Networking
# {{{ Key/Value filesave
# optional: define zkv=1 on source
[[ "$zkv" = "" ]] || {
##########################
# Key/Value file storage using ZSh associative maps
# load a map from a file
# map must be already instantiated with typeset -A by called
# name of map is defined inside the file
function zkv.load() {
fn "zkv-load $*"
file=$1
[[ "$file" = "" ]] && {
error "zkv-open() missing argument: file-path"
zerr
return 1 }
[[ -r "$file" ]] || {
error "zkv-open() file not found $file"
zerr
return 1 }
[[ -s "$file" ]] || {
error "zkv-open() file is empty"
zerr
return 1 }
source $file
}
# save a map in a file
# $1 = name of the map associative array
# $2 = full path to the file
function zkv.save() {
fn "zkv.save $*"
_map=$1
_path=$2
[[ "$_path" = "" ]] && {
error "zkv.save() missing argument: map-name path-to-file"
zerr
return 1
}
[[ -r $_path ]] && {
func "zkv.close() overwriting $_path"
func "backup turd left behind: ${_path}~"
mv $_path $_path~
}
touch $_path
# wondering about http://www.zsh.org/mla/users/2015/msg00286.html
# meanwhile solved using a double array, wasting a full map memcpy
_karr=(${(Pk)_map})
_varr=(${(Pv)_map})
_num="${#_karr}"
for c in {1..$_num}; do
# can also be cat here, however for speed we use builtins
# switch to cat if compatibility is an issue
sysread -o 1 <<EOF >> $_path
$_map+=("${_karr[$c]}" "${(v)_varr[$c]}")
EOF
done
func "$_num key/values stored in $_path"
}
}
# }}} Key/Value filesave
# {{{ Get/Set REST API
########
# Restful API client
# there is a clear zsh optimization here in get/set kv
# using zsh/tcp instead of spawning curl
# and perhaps querying with one call using ?recursive
vars+=(rest_reply_body rest_reply_header)
maps+=(rest_header)
function rest.put() {
fn "rest.put $*"
# $1 = hostname
# $2 = port
# $3 = path
# value from stdin |
# to check if the http service is running is up to the caller
_host=${1} # ip address
_port=${2}
_path=${3}
sysread _v
req=(_host)
ckreq || return $?
if ztcp $_host $_port; then
# TODO: work out various parsers, this one works with consul.io
_fd=$REPLY
# func "tcp open on fd $fd"
cat <<EOF >& $_fd
PUT ${_path} HTTP/1.1
User-Agent: Zuper/$zuper_version
Host: ${_host}:${_port}
Accept: */*
Content-Length: ${#_v}
Content-Type: application/x-www-form-urlencoded
EOF
print -n "$_v" >& $_fd
sysread -i $_fd _res
# close connection
ztcp -c $_fd
[[ "$_res" =~ "true" ]] || {
warn "failed PUT on restful key/value"
warn "host: ${_host}"
warn "port: ${_port}"
warn "path: ${_path}"
warn "value: $_v"
print - "$_res"
zerr
return 1
}
else
error "cannot connect to restful service: $_host:$_port"
zerr
return 1
fi
return 0
}
function rest.get() {
fn "rest.get $*"
_host=${1}
_port=${2}
_path=${3}
req=(_host _port)
ckreq || return $?
ztcp $_host $_port || {
zerr
return 1
}
_fd=$REPLY
# TODO: work out various parsers, this one works with consul.io
cat <<EOF >& $_fd
GET ${_path} HTTP/1.1
User-Agent: Zuper/$zuper_version
Host: $_host:$_port
Accept: */*
EOF
# read header response
rest_reply=`sysread -i $_fd -o 1`
for i in "${(f)rest_reply}"; do
print $i | hexdump -C
# first line is the response code
[[ "$i" -regex-match "\x0d\x0a$" ]] && {
func BLANK
break }
# # save other lines in map for fast retrieval
# _field=${i[(ws@:@)1]}
# func "$_field - header field parsed"
# rest_header[$_field]="${i[(ws@:@)2]}"
# c=$(( $c + 1 ))
done
# rest_reply_header="${(f)$(cat <&$_fd)}"
func "${#rest_reply_header} bytes response header stored in rest_reply_header"
# | awk -F: '
#/"Value":/ { gsub(/"|}]/,"",$7) ; print $7 }' | base64 -d
# TODO: read content-length and use it here
rest_reply_body="${(f)$(sysread -i $_fd -o 1)}"
func "${#rest_reply_body} bytes response body stored in rest_reply_body"
# close connection
ztcp -c $_fd
return 0
}
# }}} Get/Set REST API
# {{{ Helpers
[[ "$helpers" = "" ]] || {
function helper.isfound isfound() {
command -v $1 1>/dev/null 2>/dev/null
return $?
}
# remote leading and trailing spaces in a string taken from stdin
function helper.trim trim() {
sed -e 's/^[[:space:]]*//g ; s/[[:space:]]*\$//g'
}
zmodload zsh/mapfile
# faster substitute for cat
function helper.printfile printfile() {
print ${mapfile[$1]}
}
# extract all emails found in a text from stdin
# outputs them one per line
function helper.extract-emails extract_emails() {
awk '{ for (i=1;i<=NF;i++)
if ( $i ~ /[[:alnum:]]@[[:alnum:]]/ ) {
gsub(/<|>|,/ , "" , $i); print $i } }'
}
zmodload zsh/regex
# takes a string as argument, returns success if is an email
function helper.isemail isemail() {
[[ "$1" -regex-match "\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}\b" ]] && return 0
return 1
}
# takes a numeric argument and prints out a human readable size
function helper.human-size human_size() {
[[ $1 -gt 0 ]] || {
error "human_size() called with invalid argument"
return 1
}
# we use the binary operation for speed
# shift right 10 is divide by 1024
# gigabytes
[[ $1 -gt 1073741824 ]] && {
print -n "$(( $1 >> 30 )) GB"
return 0
}
# megabytes
[[ $1 -gt 1048576 ]] && {
print -n "$(( $1 >> 20 )) MB"
return 0
}
# kilobytes
[[ $1 -gt 1024 ]] && {
print -n "$(( $1 >> 10 )) KB"
return 0
}
# bytes
print -n "$1 Bytes"
return 0
}
# strips out all html/xml tags (everything between < >)
function helper.html-strip xml_strip html_strip() { sed 's/<[^>]\+>//g' }
# changes stdin string special chars to be shown in html
function helper.escape-html escape_html() {
sed -e '
s/\&/\&amp;/g
s/>/\&gt;/g
s/</\&lt;/g
s/"/\&quot;/g
'
}
# escapes special chars in urls
function helper.decode-url decode_url urldecode() {
sed -e '
s/%25/%/gi
s/%20/ /gi
s/%09/ /gi
s/%21/!/gi
s/%22/"/gi
s/%23/#/gi
s/%24/\$/gi
s/%26/\&/gi
s/%27/'\''/gi
s/%28/(/gi
s/%29/)/gi
s/%2a/\*/gi
s/%2b/+/gi
s/%2c/,/gi
s/%2d/-/gi
s/%2e/\./gi
s/%2f/\//gi
s/%3a/:/gi
s/%3b/;/gi
s/%3d/=/gi
s/%3e//gi
s/%3f/?/gi
s/%40/@/gi
s/%5b/\[/gi
s/%5c/\\/gi
s/%5d/\]/gi
s/%5e/\^/gi
s/%5f/_/gi
s/%60/`/gi
s/%7b/{/gi
s/%7c/|/gi
s/%7d/}/gi
s/%7e/~/gi
s/%09/ /gi
'
}
function helper.encode-url encode_url urlencode() {
sed -e '
s/%/%25/g
s/ /%20/g
s/ /%09/g
s/!/%21/g
s/"/%22/g
s/#/%23/g
s/\$/%24/g
s/\&/%26/g
s/'\''/%27/g
s/(/%28/g
s/)/%29/g
s/\*/%2a/g
s/+/%2b/g
s/,/%2c/g
s/-/%2d/g
s/\./%2e/g
s/\//%2f/g
s/:/%3a/g
s/;/%3b/g
s//%3e/g
s/?/%3f/g
s/@/%40/g
s/\[/%5b/g
s/\\/%5c/g
s/\]/%5d/g
s/\^/%5e/g
s/_/%5f/g
s/`/%60/g
s/{/%7b/g
s/|/%7c/g
s/}/%7d/g
s/~/%7e/g
s/ /%09/g
'
}
}
# }}} Helpers
# {{{ Config
# This is not a full config parser, but its a mechanism to read single
# sections of configuration files that are separated using various
# syntax methods. The only method supported is now org-mode whose
# sections start with #+ . It fills in the global array
# $config_section which can be read out to a file or interpreted in
# memory, whatever syntax it may contain.
vars+=(config_section_type)
arrs+=(config_section)
config_section_type=org-mode
config.section.type() {
fn config.section.type
_type=$1
req=(_type)
ckreq || return $?
case $_type in
org-mode)
config_section_type=org-mode
;;
*)
error "Unknown config type:$_type"
return 1
;;
esac
act "$_type config section parser initialized"
return 0
}
# fills in contents of section in array config_section
config.section.read() {
fn config.section.read
_file=$1
_section=$2
req=(_file _section)
freq=($_file)
ckreq || return $?
case $config_section_type in
org-mode)
_contents=`awk '
BEGIN { found=0 }
/^#\+ '"$_section"'/ { found=1; next }
/^#\+/ { if(found==1) exit 0 }
/^$/ { next }
{ if(found==1) print $0 }
' $_file`
;;
*)
error "Unknown config type:$_type"
;;
esac
config_section=()
for c in ${(f)_contents}; do
config_section+=("$c")
done
return 0
}
# }}} Config