3
# This script carries inside it multiple files. When executed, it creates
4
# the files into a temporary directory, and then calls the 'main' function.
6
# main does a run-parts of all "scripts" and then calls home to maas with
7
# maas-signal, posting output of each of the files added with add_script().
10
# If IPMI network settings have been configured statically, you can
11
# make them DHCP. If 'true', the IPMI network source will be changed
13
IPMI_CHANGE_STATIC_TO_DHCP="false"
15
# In certain hardware, the parameters for the ipmi_si kernel module
16
# might need to be specified. If you wish to send parameters, uncomment
18
#IPMI_SI_PARAMS="type=kcs ports=0xca2"
20
#### script setup ######
21
TEMP_D=$(mktemp -d "${TMPDIR:-/tmp}/${0##*/}.XXXXXX")
22
SCRIPTS_D="${TEMP_D}/scripts"
23
IPMI_CONFIG_D="${TEMP_D}/ipmi.d"
26
PATH="$BIN_D:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
29
mkdir -p "$BIN_D" "$OUT_D" "$SCRIPTS_D" "$IPMI_CONFIG_D"
31
### some utility functions ####
33
DEBIAN_FRONTEND=noninteractive apt-get --assume-yes -q "$@" </dev/null
42
chmod "${2:-755}" "${BIN_D}/$1"
45
cat > "${SCRIPTS_D}/$1"
46
chmod "${2:-755}" "${SCRIPTS_D}/$1"
49
cat > "${IPMI_CONFIG_D}/$1"
50
chmod "${2:-644}" "${IPMI_CONFIG_D}/$1"
53
[ -n "${TEMP_D}" ] || rm -Rf "${TEMP_D}"
57
local config="" file="" found=""
59
# If the config location is set in environment variable, trust it.
60
[ -n "${COMMISSIONING_CREDENTIALS_URL}" ] &&
61
_RET="${COMMISSIONING_CREDENTIALS_URL}" && return
63
# Go looking for local files written by cloud-init.
64
for file in /etc/cloud/cloud.cfg.d/*cmdline*.cfg; do
65
[ -f "$file" ] && _RET="$file" && return
68
local opt="" cmdline=""
69
if [ -f /proc/cmdline ] && read cmdline < /proc/cmdline; then
70
# Search through /proc/cmdline arguments:
71
# cloud-config-url trumps url=
72
for opt in $cmdline; do
81
[ -n "$found" ] && _RET="$found" && return 0
87
maas-signal "--config=${CRED_CFG}" "$@"
91
[ -z "$CRED_CFG" ] || signal FAILED "$1"
92
echo "FAILED: $1" 1>&2;
96
write_poweroff_job() {
97
cat >/etc/init/maas-poweroff.conf <<EOF
98
description "poweroff when maas task is done"
99
start on stopped cloud-final
103
[ ! -e /tmp/block-poweroff ] || exit 0
107
# reload required due to lack of inotify in overlayfs (LP: #882147)
108
initctl reload-configuration
114
# Install tools and load modules.
116
aptget install freeipmi-tools
119
# The main function, actually execute stuff that is written below.
120
local script total=0 creds=""
123
fail "failed to find credential config"
126
# Get remote credentials into a local file.
129
wget "$creds" -O "${TEMP_D}/my.creds" ||
130
fail "failed to get credentials from $cred_cfg"
131
creds="${TEMP_D}/my.creds"
135
# Use global name read by signal() and fail.
140
if $IPMI_CHANGE_STATIC_TO_DHCP; then
141
pargs="--dhcp-if-static"
143
power_settings=$(maas-ipmi-autodetect --configdir "$IPMI_CONFIG_D" ${pargs})
144
if [ ! -z "$power_settings" ]; then
145
signal "--power-type=ipmi" "--power-parameters=${power_settings}" WORKING "finished [maas-ipmi-autodetect]"
148
# Just get a count of how many scripts there are for progress reporting.
149
for script in "${SCRIPTS_D}/"*; do
150
[ -x "$script" -a -f "$script" ] || continue
154
local cur=1 numpass=0 name="" failed=""
155
for script in "${SCRIPTS_D}/"*; do
156
[ -f "$script" -a -f "$script" ] || continue
158
signal WORKING "starting ${script##*/} [$cur/$total]"
159
"$script" > "${OUT_D}/${name}.out" 2> "${OUT_D}/${name}.err"
161
signal WORKING "finished $name [$cur/$total]: $ret"
162
if [ $ret -eq 0 ]; then
163
numpass=$(($numpass+1))
164
failed="${failed} ${name}"
169
# Get a list of all files created, ignoring empty ones.
171
for file in "${OUT_D}/"*; do
172
[ -f "$file" -a -s "$file" ] || continue
173
fargs="$fargs --file=${file##*/}"
176
if [ $numpass -eq $total ]; then
178
signal $fargs OK "finished [$numpass/$total]" )
182
signal $fargs OK "failed [$numpass/$total] ($failed)" )
183
return $(($count-$numpass))
189
modprobe ipmi_msghandler
190
modprobe ipmi_devintf
191
modprobe ipmi_si ${IPMI_SI_PARAMS}
195
### begin writing files ###
196
add_script "01-lshw" <<"END_LSHW"
201
add_ipmi_config "02-global-config.ipmi" <<"END_IPMI_CONFIG"
203
Volatile_Access_Mode Always_Available
204
Volatile_Enable_User_Level_Auth Yes
205
Volatile_Channel_Privilege_Limit Administrator
206
Non_Volatile_Access_Mode Always_Available
207
Non_Volatile_Enable_User_Level_Auth Yes
208
Non_Volatile_Channel_Privilege_Limit Administrator
212
add_bin "maas-ipmi-autodetect" <<"END_MAAS_IPMI_AUTODETECT"
223
# TODO: Detection could be improved.
224
(status, output) = commands.getstatusoutput('ipmi-locate')
225
show_re = re.compile('(IPMI\ Version:) (\d\.\d)')
226
res = show_re.search(output)
228
found = glob.glob("/dev/ipmi[0-9]")
230
return (True, "UNKNOWN: %s" % " ".join(found))
232
return (True, res.group(2))
235
(status, output) = commands.getstatusoutput('bmc-config --checkout --key-pair="Lan_Conf:IP_Address_Source"')
236
show_re = re.compile('IP_Address_Source\s+Use_DHCP')
237
res = show_re.search(output)
242
def set_ipmi_network_source(source):
243
(status, output) = commands.getstatusoutput('bmc-config --commit --key-pair="Lan_Conf:IP_Address_Source=%s"' % source)
245
def get_ipmi_ip_address():
246
(status, output) = commands.getstatusoutput('bmc-config --checkout --key-pair="Lan_Conf:IP_Address"')
247
show_re = re.compile('([0-9]{1,3}[.]?){4}')
248
res = show_re.search(output)
251
def get_ipmi_user_number(user):
252
for i in range(1, 17):
253
ipmi_user_number = "User%s" % i
254
(status, output) = commands.getstatusoutput('bmc-config --checkout --key-pair="%s:Username"' % ipmi_user_number)
256
return ipmi_user_number
259
def commit_ipmi_user_settings(user, password):
260
ipmi_user_number = get_ipmi_user_number(user)
261
if ipmi_user_number is None:
262
(status, output) = commands.getstatusoutput('bmc-config --commit --key-pair="User10:Username=%s"' % user)
263
ipmi_user_number = get_ipmi_user_number(user)
264
(status, output) = commands.getstatusoutput('bmc-config --commit --key-pair="%s:Password=%s"' % (ipmi_user_number, password))
265
(status, output) = commands.getstatusoutput('bmc-config --commit --key-pair="%s:Enable_User=Yes"' % ipmi_user_number)
266
(status, output) = commands.getstatusoutput('bmc-config --commit --key-pair="%s:Lan_Enable_IPMI_Msgs=Yes"' % ipmi_user_number)
267
(status, output) = commands.getstatusoutput('bmc-config --commit --key-pair="%s:Lan_Privilege_Limit=Administrator"' % ipmi_user_number)
269
def commit_ipmi_settings(config):
270
(status, output) = commands.getstatusoutput('bmc-config --commit --filename %s' % config)
272
def get_maas_power_settings(user, password, ipaddress):
273
return "%s,%s,%s" % (user, password, ipaddress)
275
def generate_random_password(min=8,max=15):
276
length=random.randint(min,max)
277
letters=string.ascii_letters+string.digits
278
return ''.join([random.choice(letters) for _ in range(length)])
284
parser = argparse.ArgumentParser(
285
description='send config file to modify IPMI settings with')
286
parser.add_argument("--configdir", metavar="folder",
287
help="specify config file", default=None)
288
parser.add_argument("--dhcp-if-static", action="store_true",
289
dest="dhcp", help="specify config file", default=False)
291
args = parser.parse_args()
293
# Check whether IPMI exists or not.
294
(status, ipmi_version) = detect_ipmi()
296
# if False, then failed to detect ipmi
299
# Check whether IPMI is being set to DHCP. If it is not, and
300
# '--dhcp-if-static' has been passed, Set it to IPMI to DHCP.
301
if not is_ipmi_dhcp() and args.dhcp:
302
set_ipmi_network_source("Use_DHCP")
303
# allow IPMI 120 seconds to obtain an IP address
307
IPMI_MAAS_USER="maas"
308
IPMI_MAAS_PASSWORD=generate_random_password()
310
# Configure IPMI user/password
311
commit_ipmi_user_settings(IPMI_MAAS_USER, IPMI_MAAS_PASSWORD)
313
# Commit other IPMI settings
315
for file in os.listdir(args.configdir):
316
commit_ipmi_settings(os.path.join(args.configdir, file))
319
IPMI_IP_ADDRESS = get_ipmi_ip_address()
320
if IPMI_IP_ADDRESS == "0.0.0.0":
321
# if IPMI_IP_ADDRESS is 0.0.0.0, wait 60 seconds and retry.
322
set_ipmi_network_source("Static")
324
set_ipmi_network_source("Use_DHCP")
326
IPMI_IP_ADDRESS = get_ipmi_ip_address()
328
if IPMI_IP_ADDRESS is None or IPMI_IP_ADDRESS == "0.0.0.0":
329
# Exit (to not set power params in MAAS) if no IPMI_IP_ADDRESS
333
print get_maas_power_settings(IPMI_MAAS_USER, IPMI_MAAS_PASSWORD, IPMI_IP_ADDRESS)
335
if __name__ == '__main__':
337
END_MAAS_IPMI_AUTODETECT
339
add_bin "maas-signal" <<"END_MAAS_SIGNAL"
342
from email.utils import parsedate
344
import oauth.oauth as oauth
354
MD_VERSION = "2012-03-01"
355
VALID_STATUS = ("OK", "FAILED", "WORKING")
356
POWER_TYPES = ("ipmi", "virsh", "ether_wake")
359
def _encode_field(field_name, data, boundary):
360
return ('--' + boundary,
361
'Content-Disposition: form-data; name="%s"' % field_name,
365
def _encode_file(name, fileObj, boundary):
366
return ('--' + boundary,
367
'Content-Disposition: form-data; name="%s"; filename="%s"' %
369
'Content-Type: %s' % _get_content_type(name),
373
def _random_string(length):
374
return ''.join(random.choice(string.letters) for ii in range(length + 1))
377
def _get_content_type(filename):
378
return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
381
def encode_multipart_data(data, files):
382
"""Create a MIME multipart payload from L{data} and L{files}.
384
@param data: A mapping of names (ASCII strings) to data (byte string).
385
@param files: A mapping of names (ASCII strings) to file objects ready to
387
@return: A 2-tuple of C{(body, headers)}, where C{body} is a a byte string
388
and C{headers} is a dict of headers to add to the enclosing request in
389
which this payload will travel.
391
boundary = _random_string(30)
395
lines.extend(_encode_field(name, data[name], boundary))
397
lines.extend(_encode_file(name, files[name], boundary))
398
lines.extend(('--%s--' % boundary, ''))
399
body = '\r\n'.join(lines)
401
headers = {'content-type': 'multipart/form-data; boundary=' + boundary,
402
'content-length': str(len(body))}
407
def oauth_headers(url, consumer_key, token_key, token_secret, consumer_secret,
409
consumer = oauth.OAuthConsumer(consumer_key, consumer_secret)
410
token = oauth.OAuthToken(token_key, token_secret)
412
timestamp = int(time.time()) + clockskew
415
'oauth_version': "1.0",
416
'oauth_nonce': oauth.generate_nonce(),
417
'oauth_timestamp': timestamp,
418
'oauth_token': token.key,
419
'oauth_consumer_key': consumer.key,
421
req = oauth.OAuthRequest(http_url=url, parameters=params)
422
req.sign_request(oauth.OAuthSignatureMethod_PLAINTEXT(),
424
return(req.to_header())
427
def geturl(url, creds, headers=None, data=None):
428
# Takes a dict of creds to be passed through to oauth_headers,
429
# so it should have consumer_key, token_key, ...
433
headers = dict(headers)
438
sys.stderr.write(msg + "\n")
440
exc = Exception("Unexpected Error")
441
for naptime in (1, 1, 2, 4, 8, 16, 32):
442
if creds.get('consumer_key', None) != None:
443
headers.update(oauth_headers(url,
444
consumer_key=creds['consumer_key'],
445
token_key=creds['token_key'],
446
token_secret=creds['token_secret'],
447
consumer_secret=creds['consumer_secret'],
448
clockskew=clockskew))
450
req = urllib2.Request(url=url, data=data, headers=headers)
451
return(urllib2.urlopen(req).read())
452
except urllib2.HTTPError as exc:
453
if 'date' not in exc.headers:
454
warn("date field not in %d headers" % exc.code)
456
elif (exc.code == 401 or exc.code == 403):
457
date = exc.headers['date']
459
ret_time = time.mktime(parsedate(date))
460
clockskew = int(ret_time - time.time())
461
warn("updated clock skew to %d" % clockskew)
463
warn("failed to convert date '%s'" % date)
464
except Exception as exc:
467
warn("request to %s failed. sleeping %d.: %s" % (url, naptime, exc))
473
def read_config(url, creds):
474
if url.startswith("http://") or url.startswith("https://"):
475
cfg_str = urllib2.urlopen(urllib2.Request(url=url))
477
if url.startswith("file://"):
479
cfg_str = open(url,"r").read()
481
cfg = yaml.safe_load(cfg_str)
483
# Support reading cloud-init config for MAAS datasource.
484
if 'datasource' in cfg:
485
cfg = cfg['datasource']['MAAS']
487
for key in creds.keys():
488
if key in cfg and creds[key] == None:
489
creds[key] = cfg[key]
492
sys.stderr.write("FAIL: %s" % msg)
498
Call with single argument of directory or http or https url.
499
If url is given additional arguments are allowed, which will be
500
interpreted as consumer_key, token_key, token_secret, consumer_secret.
505
parser = argparse.ArgumentParser(
506
description='Send signal operation and optionally post files to MAAS')
507
parser.add_argument("--config", metavar="file",
508
help="Specify config file", default=None)
509
parser.add_argument("--ckey", metavar="key",
510
help="The consumer key to auth with", default=None)
511
parser.add_argument("--tkey", metavar="key",
512
help="The token key to auth with", default=None)
513
parser.add_argument("--csec", metavar="secret",
514
help="The consumer secret (likely '')", default="")
515
parser.add_argument("--tsec", metavar="secret",
516
help="The token secret to auth with", default=None)
517
parser.add_argument("--apiver", metavar="version",
518
help="The apiver to use ("" can be used)", default=MD_VERSION)
519
parser.add_argument("--url", metavar="url",
520
help="The data source to query", default=None)
521
parser.add_argument("--file", dest='files',
522
help="File to post", action='append', default=[])
523
parser.add_argument("--post", dest='posts',
524
help="name=value pairs to post", action='append', default=[])
525
parser.add_argument("--power-type", dest='power_type',
526
help="Power type.", choices=POWER_TYPES, default=None)
527
parser.add_argument("--power-parameters", dest='power_parms',
528
help="Power parameters.", default=None)
530
parser.add_argument("status",
531
help="Status", choices=VALID_STATUS, action='store')
532
parser.add_argument("message", help="Optional message",
533
default="", nargs='?')
535
args = parser.parse_args()
537
creds = {'consumer_key': args.ckey, 'token_key': args.tkey,
538
'token_secret': args.tsec, 'consumer_secret': args.csec,
539
'metadata_url': args.url}
542
read_config(args.config, creds)
544
url = creds.get('metadata_url', None)
546
fail("URL must be provided either in --url or in config\n")
547
url = "%s/%s/" % (url, args.apiver)
551
"status": args.status,
552
"error": args.message}
554
for ent in args.posts:
556
(key, val) = ent.split("=", 2)
558
sys.stderr.write("'%s' had no '='" % ent)
562
if args.power_parms is not None:
563
params["power_type"] = args.power_type
565
power_user=args.power_parms.split(",")[0],
566
power_pass=args.power_parms.split(",")[1],
567
power_address=args.power_parms.split(",")[2]
569
params["power_parameters"] = json.dumps(power_parms)
572
for fpath in args.files:
573
files[os.path.basename(fpath)] = open(fpath, "r")
575
data, headers = encode_multipart_data(params, files)
581
payload = geturl(url, creds=creds, headers=headers, data=data)
583
raise TypeError("Unexpected result from call: %s" % payload)
586
except urllib2.HTTPError as exc:
587
msg = "http error [%s]" % exc.code
588
except urllib2.URLError as exc:
589
msg = "url error [%s]" % exc.reason
590
except socket.timeout as exc:
591
msg = "socket timeout [%s]" % exc
592
except TypeError as exc:
594
except Exception as exc:
595
msg = "unexpected error [%s]" % exc
597
sys.stderr.write("%s\n" % msg)
598
sys.exit((exc is None))
600
if __name__ == '__main__':