launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #07091
[Merge] lp:~smoser/maas/useful-commissioning-script into lp:maas
Scott Moser has proposed merging lp:~smoser/maas/useful-commissioning-script into lp:maas.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~smoser/maas/useful-commissioning-script/+merge/101679
add a commissioning user data that actually does something useful
This adds a user-data script that is a shell script. Inside it are multiple files
that will be unpacked and written to a temporary directory.
files written with 'add_script' will be executed in run-parts style.
files written with 'add_bin' will be put in the main scripts path (for use from 'main()')
the maas-signal program added is able to call home to maas with progress and upload files.
I've also chosen to rename 'commissioning.sh' to 'commissioning-user-data' which is more correct for what this actually is. This happens to be posix sh, but there is
no requirement on that. It could be python or cloud-init multipart data.
--
https://code.launchpad.net/~smoser/maas/useful-commissioning-script/+merge/101679
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~smoser/maas/useful-commissioning-script into lp:maas.
=== renamed file 'etc/maas/commissioning.sh' => 'etc/maas/commisioning-user-data'
--- etc/maas/commissioning.sh 2012-04-12 03:13:25 +0000
+++ etc/maas/commisioning-user-data 2012-04-12 04:24:17 +0000
@@ -1,3 +1,346 @@
#!/bin/sh
-echo "Hello world"
-
+#
+# This script carries inside it multiple files. When executed, it creates
+# the files into a temporary directory, and then calls the 'main' function
+#
+# main does a run-parts of all "scripts" and then calls home to maas with
+# maas-signal, posting output of each of the files added with add_script()
+#
+#### script setup ######
+TEMP_D=$(mktemp -d "${TMPDIR:-/tmp}/${0##*/}.XXXXXX")
+SCRIPTS_D="${TEMP_D}/scripts"
+BIN_D="${TEMP_D}/bin"
+OUT_D="${TEMP_D}/out"
+PATH="$BIN_D:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+trap cleanup EXIT
+
+mkdir -p "$BIN_D" "$OUT_D" "$SCRIPTS_D"
+
+### some utility functions ####
+writefile() {
+ cat > "$1"
+ chmod "$2" "$1"
+}
+add_bin() {
+ cat > "${BIN_D}/$1"
+ chmod "${2:-755}" "${BIN_D}/$1"
+}
+add_script() {
+ cat > "${SCRIPTS_D}/$1"
+ chmod "${2:-755}" "${SCRIPTS_D}/$1"
+}
+cleanup() {
+ [ -n "${TEMP_D}" ] || rm -Rf "${TEMP_D}"
+}
+
+find_creds_cfg() {
+ local config="" file="" found=""
+
+ # if the config location is set in environment variable, trust it
+ [ -n "${COMMISSIONING_CREDENTIALS_URL}" ] &&
+ _RET="${COMMISSIONING_CREDENTIALS_URL}" && return
+
+ # go looking for local files written by cloud-init
+ for file in /etc/cloud/cloud.cfg.d/*cmdline*.cfg; do
+ [ -f "$file" ] && _RET="$file" && return
+ done
+
+ local opt="" cmdline=""
+ if [ -f /proc/cmdline ] && read cmdline < /proc/cmdline; then
+ # search through /proc/cmdline arguments
+ # cloud-config-url trumps url=
+ for opt in $cmdline; do
+ case "$opt" in
+ url=*)
+ found=${opt#url=};;
+ cloud-config-url=*)
+ _RET="${opt#*=}"
+ return 0;;
+ esac
+ done
+ fi
+}
+
+signal() {
+ maas-signal "--config=${CRED_CFG}" "$@"
+}
+
+fail() {
+ [ -z "$CRED_CFG" ] || signal FAILED "$1"
+ echo "FAILED: $1" 1>&2;
+ exit 1
+}
+
+main() {
+ # the main function, actually execute stuff that is written below
+ local script total=0 creds=""
+
+ find_creds_cfg ||
+ fail "failed to find credential config"
+ creds="$_RET"
+
+ # get remote credentials into a local file
+ case "$creds" in
+ http://*|https://*)
+ wget "$creds" -O "${TEMP_D}/my.creds" ||
+ fail "failed to get credentials from $cred_cfg"
+ creds="${TEMP_D}/my.creds"
+ ;;
+ esac
+
+ # use global name read by signal() and fail
+ CRED_CFG="$creds"
+
+ # just get a count of how many scripts there are for progress reporting
+ for script in "${SCRIPTS_D}/"*; do
+ [ -x "$script" -a -f "$script" ] || continue
+ total=$(($total+1))
+ done
+
+ local cur=1 numpass=0 name="" failed=""
+ for script in "${SCRIPTS_D}/"*; do
+ [ -f "$script" -a -f "$script" ] || continue
+ name=${script##*/}
+ signal WORKING "starting ${script##*/} [$cur/$total]"
+ "$script" > "${OUT_D}/${name}.out" 2> "${OUT_D}/${name}.err"
+ ret=$?
+ signal WORKING "finished $name [$cur/$total]: $ret"
+ if [ $ret -eq 0 ]; then
+ numpass=$(($numpass+1))
+ failed="${failed} ${name}"
+ fi
+ cur=$(($cur+1))
+ done
+
+ # get a list of all files created, ignoring empty ones
+ local fargs=""
+ for file in "${OUT_D}/"*; do
+ [ -f "$file" -a -s "$file" ] || continue
+ fargs="$fargs --file=${file##*/}"
+ done
+
+ if [ $numpass -eq $total ]; then
+ ( cd "${OUT_D}" &&
+ signal $fargs OK "finished [$passed/$count]" )
+ return 0
+ else
+ ( cd "${OUT_D}" &&
+ signal $fargs OK "failed [$passed/$count] ($failed)" )
+ return $(($count-$numpass))
+ fi
+
+}
+
+### begin writing files ###
+add_script "01-lshw" <<"END_LSHW"
+#!/bin/sh
+lshw -xml
+END_LSHW
+
+add_bin "maas-signal" <<"END_MAAS_SIGNAL"
+#!/usr/bin/python
+
+import mimetypes
+import oauth.oauth as oauth
+import os.path
+import random
+import string
+import sys
+import time
+import urllib2
+import yaml
+
+MD_VERSION = "2012-03-01"
+VALID_STATUS = ("OK", "FAILED", "WORKING")
+
+
+def _encode_field(field_name, data, boundary):
+ return ('--' + boundary,
+ 'Content-Disposition: form-data; name="%s"' % field_name,
+ '', str(data))
+
+
+def _encode_file(name, fileObj, boundary):
+ return ('--' + boundary,
+ 'Content-Disposition: form-data; name="%s"; filename="%s"' %
+ (name, name),
+ 'Content-Type: %s' % _get_content_type(name),
+ '', fileObj.read())
+
+
+def _random_string(length):
+ return ''.join(random.choice(string.letters) for ii in range(length + 1))
+
+
+def _get_content_type(filename):
+ return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
+
+
+def encode_multipart_data(data, files):
+ """Create a MIME multipart payload from L{data} and L{files}.
+
+ @param data: A mapping of names (ASCII strings) to data (byte string).
+ @param files: A mapping of names (ASCII strings) to file objects ready to
+ be read.
+ @return: A 2-tuple of C{(body, headers)}, where C{body} is a a byte string
+ and C{headers} is a dict of headers to add to the enclosing request in
+ which this payload will travel.
+ """
+ boundary = _random_string(30)
+
+ lines = []
+ for name in data:
+ lines.extend(_encode_field(name, data[name], boundary))
+ for name in files:
+ lines.extend(_encode_file(name, files[name], boundary))
+ lines.extend(('--%s--' % boundary, ''))
+ body = '\r\n'.join(lines)
+
+ headers = {'content-type': 'multipart/form-data; boundary=' + boundary,
+ 'content-length': str(len(body))}
+
+ return body, headers
+
+
+def oauth_headers(url, consumer_key, token_key, token_secret, consumer_secret):
+ consumer = oauth.OAuthConsumer(consumer_key, consumer_secret)
+ token = oauth.OAuthToken(token_key, token_secret)
+ params = {
+ 'oauth_version': "1.0",
+ 'oauth_nonce': oauth.generate_nonce(),
+ 'oauth_timestamp': int(time.time()),
+ 'oauth_token': token.key,
+ 'oauth_consumer_key': consumer.key,
+ }
+ req = oauth.OAuthRequest(http_url=url, parameters=params)
+ req.sign_request(oauth.OAuthSignatureMethod_PLAINTEXT(),
+ consumer, token)
+ return(req.to_header())
+
+
+def geturl(url, creds, headers=None, data=None):
+ # takes a dict of creds to be passed through to oauth_headers
+ # so it should have consumer_key, token_key, ...
+ if headers is None:
+ headers = {}
+ else:
+ headers = dict(headers)
+
+ if creds.get('consumer_key', None) != None:
+ headers.update(oauth_headers(url,
+ consumer_key=creds['consumer_key'], token_key=creds['token_key'],
+ token_secret=creds['token_secret'],
+ consumer_secret=creds['consumer_secret']))
+ req = urllib2.Request(url=url, data=data, headers=headers)
+ return(urllib2.urlopen(req).read())
+
+def read_config(url, creds):
+ if url.startswith("http://") or url.startswith("https://"):
+ cfg_str = urllib2.urlopen(urllib2.Request(url=url))
+ else:
+ if url.startswith("file://"):
+ url = url[7:]
+ cfg_str = open(url,"r").read()
+
+ cfg = yaml.load(cfg_str)
+
+ # support reading cloud-init config for MAAS datasource
+ if 'datasource' in cfg:
+ cfg = cfg['datasource']['MAAS']
+
+ for key in creds.keys():
+ if key in cfg and creds[key] == None:
+ creds[key] = cfg[key]
+
+def fail(msg):
+ sys.stderr.write("FAIL: %s" % msg)
+ sys.exit(1)
+
+
+def main():
+ """
+ Call with single argument of directory or http or https url.
+ If url is given additional arguments are allowed, which will be
+ interpreted as consumer_key, token_key, token_secret, consumer_secret
+ """
+ import argparse
+ import pprint
+
+ parser = argparse.ArgumentParser(
+ description='send signal operation and optionally post files to MAAS')
+ parser.add_argument("--config", metavar="file",
+ help="specify config file", default=None)
+ parser.add_argument("--ckey", metavar="key",
+ help="the consumer key to auth with", default=None)
+ parser.add_argument("--tkey", metavar="key",
+ help="the token key to auth with", default=None)
+ parser.add_argument("--csec", metavar="secret",
+ help="the consumer secret (likely '')", default="")
+ parser.add_argument("--tsec", metavar="secret",
+ help="the token secret to auth with", default=None)
+ parser.add_argument("--apiver", metavar="version",
+ help="the apiver to use ("" can be used)", default=MD_VERSION)
+ parser.add_argument("--url", metavar="url",
+ help="the data source to query", default=None)
+ parser.add_argument("--file", dest='files',
+ help="file to post", action='append', default=[])
+
+ parser.add_argument("status",
+ help="status", choices=VALID_STATUS, action='store')
+ parser.add_argument("message", help="optional message",
+ default="", nargs='?')
+
+ args = parser.parse_args()
+
+ creds = {'consumer_key': args.ckey, 'token_key': args.tkey,
+ 'token_secret': args.tsec, 'consumer_secret': args.csec,
+ 'metadata_url': args.url}
+
+ if args.config:
+ read_config(args.config, creds)
+
+ url = creds.get('metadata_url', None)
+ if not url:
+ fail("Url must be provided either in --url or in config\n")
+ url = "%s/%s/" % (url, args.apiver)
+
+ params = {
+ "op": "signal",
+ "status": args.status,
+ "error": args.message}
+
+ files = {}
+ for fpath in args.files:
+ files[os.path.basename(fpath)] = open(fpath, "r")
+
+ data, headers = encode_multipart_data(params, files)
+
+ exc = None
+ msg = ""
+
+ try:
+ payload = geturl(url, creds=creds, headers=headers, data=data)
+ if payload != "OK":
+ raise TypeError("Unexpected result from call: %s" % payload)
+ else:
+ msg = "Success"
+ except urllib2.HTTPError as exc:
+ msg = "http error [%s]" % exc.code
+ except urllib2.URLError as exc:
+ msg = "url error [%s]" % exc.reason
+ except socket.timeout as exc:
+ msg = "socket timeout [%s]" % exc
+ except TypeError as exc:
+ msg = exc.message
+ except Exception as exc:
+ msg = "unexpected error [%s]" % exc
+
+ sys.stderr.write("%s\n" % msg)
+ sys.exit((exc is None))
+
+if __name__ == '__main__':
+ main()
+END_MAAS_SIGNAL
+
+main
+exit
=== modified file 'src/maas/settings.py'
--- src/maas/settings.py 2012-04-12 03:13:25 +0000
+++ src/maas/settings.py 2012-04-12 04:24:17 +0000
@@ -265,7 +265,7 @@
# The location of the commissioning script that is executed on nodes as
# part of commissioning. Only override this if you know what you are
# doing.
-COMMISSIONING_SCRIPT = 'etc/maas/commissioning.sh'
+COMMISSIONING_SCRIPT = 'etc/maas/commissioning-user-data'
# Allow the user to override settings in maas_local_settings.
import_local_settings()