root / plugins / network / ath9k_ @ 14ff36a3
Historique | Voir | Annoter | Télécharger (19 ko)
| 1 |
#!/bin/sh |
|---|---|
| 2 |
# weird shebang? See below: "interpreter selection" |
| 3 |
# |
| 4 |
# Collect information related to ath9k wireless events and states. |
| 5 |
# * rate control statistics ("rc_stats")
|
| 6 |
# * events (dropped, transmitted, beacon loss, ...) |
| 7 |
# * traffic (packets, bytes) |
| 8 |
# |
| 9 |
# All data is collected for each separate station (in case of multiple |
| 10 |
# connected peers). Combined graphs are provided as a summary. |
| 11 |
# |
| 12 |
# |
| 13 |
# This plugin works with the following python interpreters: |
| 14 |
# * Python 3 |
| 15 |
# * micropython |
| 16 |
# |
| 17 |
# |
| 18 |
# Copyright (C) 2015 Lars Kruse <devel@sumpfralle.de> |
| 19 |
# |
| 20 |
# This program is free software: you can redistribute it and/or modify |
| 21 |
# it under the terms of the GNU General Public License as published by |
| 22 |
# the Free Software Foundation, either version 3 of the License, or |
| 23 |
# (at your option) any later version. |
| 24 |
# |
| 25 |
# This program is distributed in the hope that it will be useful, |
| 26 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 27 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 28 |
# GNU General Public License for more details. |
| 29 |
# |
| 30 |
# You should have received a copy of the GNU General Public License |
| 31 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 32 |
# |
| 33 |
# Magic markers |
| 34 |
#%# capabilities=autoconf suggest |
| 35 |
#%# family=auto |
| 36 |
|
| 37 |
"""true" |
| 38 |
# ****************** Interpreter Selection *************** |
| 39 |
# This unbelievable dirty hack allows to find a suitable python interpreter. |
| 40 |
# This is specifically useful for OpenWRT where typically only micropython is available. |
| 41 |
# |
| 42 |
# This "execution hack" works as follows: |
| 43 |
# * the script is executed by busybox ash or another shell |
| 44 |
# * the above line (three quotes before and one quote after 'true') evaluates differently for shell and python: |
| 45 |
# * shell: run "true" (i.e. nothing happens) |
| 46 |
# * python: ignore everything up to the next three consecutive quotes |
| 47 |
# Thus we may place shell code here that will take care for selecting an interpreter. |
| 48 |
|
| 49 |
# prefer micropython if it is available - otherwise fall back to any python (2 or 3) |
| 50 |
if which micropython >/dev/null; then |
| 51 |
/usr/bin/micropython "$0" "$@" |
| 52 |
else |
| 53 |
python3 "$0" "$@" |
| 54 |
fi |
| 55 |
exit $? |
| 56 |
|
| 57 |
# For shell: ignore everything starting from here until the last line of this file. |
| 58 |
# This is necessary for syntax checkers that try to complain about invalid shell syntax below. |
| 59 |
true <<EOF |
| 60 |
""" |
| 61 |
|
| 62 |
|
| 63 |
""" |
| 64 |
The following graphs are generated for each physical ath9k interface: |
| 65 |
phy0_wifi0_traffic |
| 66 |
phy0_wifi0_traffic.station0 |
| 67 |
... |
| 68 |
pyh0_wifi0_events |
| 69 |
phy0_wifi0_events.station0 |
| 70 |
... |
| 71 |
pyh0_wifi0_rc_stats |
| 72 |
phy0_wifi0_rc_stats.station0 |
| 73 |
... |
| 74 |
""" |
| 75 |
|
| 76 |
|
| 77 |
plugin_version = "0.2" |
| 78 |
|
| 79 |
STATION_TRAFFIC_COUNTERS = ("rx_bytes", "tx_bytes", "rx_packets", "tx_packets")
|
| 80 |
STATION_EVENT_COUNTERS = ("tx_retry_count", "tx_retry_failed", "tx_filtered", "tx_fragments",
|
| 81 |
"rx_dropped", "rx_fragments", "rx_duplicates", "beacon_loss_count") |
| 82 |
# 16 colors (see http://munin-monitoring.org/wiki/fieldname.colour) for visualizing |
| 83 |
# rate control selection (see rc_stats) |
| 84 |
QUALITY_GRAPH_COLORS_16 = ("FF1F00", "FF4500", "FF7000", "FF9700",
|
| 85 |
"FFBC00", "FAE600", "D1FF00", "7BFF00", |
| 86 |
"1CFF00", "06E41B", "00C43B", "009D60", |
| 87 |
"007986", "0058A8", "0033CC", "0018DE") |
| 88 |
SYS_BASE_DIR = "/sys/kernel/debug/ieee80211" |
| 89 |
GRAPH_BASE_NAME = "ath9k_stats" |
| 90 |
PLUGIN_SCOPES = ("traffic", "events", "rcstats")
|
| 91 |
|
| 92 |
|
| 93 |
import os |
| 94 |
import os.path |
| 95 |
import subprocess |
| 96 |
import sys |
| 97 |
|
| 98 |
|
| 99 |
class Station: |
| 100 |
|
| 101 |
config_map = {"events": lambda station, **kwargs: station._get_events_config(**kwargs),
|
| 102 |
"traffic": lambda station, **kwargs: station._get_traffic_config(**kwargs), |
| 103 |
"rcstats": lambda station, **kwargs: station._get_rc_stats_config(**kwargs)} |
| 104 |
values_map = {"events": lambda station: station._events_stats,
|
| 105 |
"traffic": lambda station: station._traffic_stats, |
| 106 |
"rcstats": lambda station: station._get_rc_stats_success()} |
| 107 |
|
| 108 |
def __init__(self, label, key, path): |
| 109 |
self._path = path |
| 110 |
self.label = label |
| 111 |
self.key = key |
| 112 |
self._events_stats = self._parse_file_based_stats(STATION_EVENT_COUNTERS) |
| 113 |
self._traffic_stats = self._parse_file_based_stats(STATION_TRAFFIC_COUNTERS) |
| 114 |
self._rc_stats = self._parse_rc_stats() |
| 115 |
|
| 116 |
def _parse_rc_stats(self): |
| 117 |
""" example content |
| 118 |
|
| 119 |
type rate tpt eprob *prob ret *ok(*cum) ok( cum) |
| 120 |
HT20/LGI MCS0 5.6 100.0 100.0 3 0( 0) 3( 3) |
| 121 |
HT20/LGI MCS1 10.5 100.0 100.0 0 0( 0) 1( 1) |
| 122 |
HT20/LGI MCS2 14.9 100.0 100.0 0 0( 0) 1( 1) |
| 123 |
HT20/LGI MCS3 18.7 96.5 100.0 5 0( 0) 261( 328) |
| 124 |
HT20/LGI MCS4 25.3 95.6 100.0 5 0( 0) 4267( 5460) |
| 125 |
HT20/LGI MCS5 30.6 95.8 100.0 5 0( 0) 11735( 17482) |
| 126 |
HT20/LGI MCS6 32.9 95.7 100.0 5 0( 0) 24295( 32592) |
| 127 |
HT20/LGI DP MCS7 35.0 90.4 95.2 5 0( 0) 63356( 88600) |
| 128 |
HT20/LGI MCS8 10.5 100.0 100.0 0 0( 0) 1( 1) |
| 129 |
|
| 130 |
beware: sometimes the last two pairs of columns are joined without withespace: "90959383(100188029)" |
| 131 |
""" |
| 132 |
stats = {}
|
| 133 |
with open(os.path.join(self._path, "rc_stats"), "r") as statsfile: |
| 134 |
rate_column = None |
| 135 |
skip_retry_column = False |
| 136 |
for index, line in enumerate(statsfile.readlines()): |
| 137 |
# remove trailing linebreak, replace braces (annoyingly present in the lasf four columns) |
| 138 |
line = line.rstrip().replace("(", " ").replace(")", " ")
|
| 139 |
# ignore the trailing summary lines |
| 140 |
if not line: |
| 141 |
break |
| 142 |
if index == 0: |
| 143 |
# we need to remember the start of the "rate" column (in order to skip the flags) |
| 144 |
rate_column = line.index("rate")
|
| 145 |
if rate_column == 0: |
| 146 |
# the following weird format was found on a Barrier Breaker host (2014, Linux 3.10.49): |
| 147 |
# rate throughput ewma prob this prob this succ/attempt success attempts |
| 148 |
# ABCDP 6 5.4 89.9 100.0 0( 0) 171 183 |
| 149 |
# Thus we just assume that there are five flag letters and two blanks. |
| 150 |
# Let's hope for the best! |
| 151 |
rate_column = 6 |
| 152 |
# this format does not contain the "retry" column |
| 153 |
skip_retry_column = True |
| 154 |
# skip the header line |
| 155 |
continue |
| 156 |
cutoff_line = line[rate_column:] |
| 157 |
tokens = cutoff_line.split() |
| 158 |
entry = {}
|
| 159 |
entry["rate"] = tokens.pop(0) |
| 160 |
entry["throughput"] = float(tokens.pop(0)) |
| 161 |
entry["ewma_probability"] = float(tokens.pop(0)) |
| 162 |
entry["this_probability"] = float(tokens.pop(0)) |
| 163 |
if skip_retry_column: |
| 164 |
entry["retry"] = 0 |
| 165 |
else: |
| 166 |
entry["retry"] = int(tokens.pop(0)) |
| 167 |
entry["this_success"] = int(tokens.pop(0)) |
| 168 |
entry["this_attempts"] = int(tokens.pop(0)) |
| 169 |
entry["success"] = int(tokens.pop(0)) |
| 170 |
entry["attempts"] = int(tokens.pop(0)) |
| 171 |
# some "rate" values are given in MBit/s - some are MCS0..15 |
| 172 |
try: |
| 173 |
entry["rate_label"] = "{rate:d} MBit/s".format(rate=int(entry["rate"]))
|
| 174 |
except ValueError: |
| 175 |
# keep the MCS string |
| 176 |
entry["rate_label"] = entry["rate"] |
| 177 |
stats[entry["rate"]] = entry |
| 178 |
return stats |
| 179 |
|
| 180 |
def _get_rc_stats_success(self): |
| 181 |
rc_values = {self._get_rate_fieldname(rate["rate"]): rate["success"] for rate in self._rc_stats.values()}
|
| 182 |
rc_values["sum"] = sum(rc_values.values()) |
| 183 |
return rc_values |
| 184 |
|
| 185 |
def _parse_file_based_stats(self, counters): |
| 186 |
stats = {}
|
| 187 |
for counter in counters: |
| 188 |
# some events are not handled with older versions (e.g. "beacon_loss_count") |
| 189 |
filename = os.path.join(self._path, counter) |
| 190 |
if os.path.exists(filename): |
| 191 |
content = open(filename, "r").read().strip() |
| 192 |
stats[counter] = int(content) |
| 193 |
return stats |
| 194 |
|
| 195 |
def get_values(self, scope, graph_base): |
| 196 |
func = self.values_map[scope] |
| 197 |
yield "multigraph {base}_{suffix}.{station}".format(base=graph_base, suffix=scope, station=self.key)
|
| 198 |
for key, value in func(self).items(): |
| 199 |
yield "{key}.value {value}".format(key=key, value=value)
|
| 200 |
yield "" |
| 201 |
|
| 202 |
@classmethod |
| 203 |
def get_summary_values(cls, scope, siblings, graph_base): |
| 204 |
func = cls.values_map[scope] |
| 205 |
yield "multigraph {base}_{suffix}".format(base=graph_base, suffix=scope)
|
| 206 |
stats = {}
|
| 207 |
for station in siblings: |
| 208 |
for key, value in func(station).items(): |
| 209 |
stats[key] = stats.get(key, 0) + value |
| 210 |
for key, value in stats.items(): |
| 211 |
yield "{key}.value {value}".format(key=key, value=value)
|
| 212 |
yield "" |
| 213 |
|
| 214 |
def get_config(self, scope, graph_base): |
| 215 |
func = self.config_map[scope] |
| 216 |
yield "multigraph {base}_{suffix}.{station}".format(base=graph_base, suffix=scope, station=self.key)
|
| 217 |
yield from func(self, label=self.label, siblings=[self]) |
| 218 |
|
| 219 |
@classmethod |
| 220 |
def get_summary_config(cls, scope, siblings, graph_base): |
| 221 |
func = cls.config_map[scope] |
| 222 |
yield "multigraph {base}_{suffix}".format(base=graph_base, suffix=scope)
|
| 223 |
for station in siblings: |
| 224 |
yield from func(station, siblings=[station]) |
| 225 |
|
| 226 |
@classmethod |
| 227 |
def _get_traffic_config(cls, label=None, siblings=None): |
| 228 |
if label: |
| 229 |
yield "graph_title ath9k Station Traffic {label}".format(label=label)
|
| 230 |
else: |
| 231 |
yield "graph_title ath9k Station Traffic" |
| 232 |
yield "graph_args --base 1024" |
| 233 |
yield "graph_vlabel received (-) / transmitted (+)" |
| 234 |
yield "graph_category wireless" |
| 235 |
# convert bytes/s into kbit/s (x * 8 / 1000 = x / 125) |
| 236 |
yield from _get_up_down_pair("kBit/s", "tx_bytes", "rx_bytes", divider=125, use_negative=False)
|
| 237 |
yield from _get_up_down_pair("Packets/s", "tx_packets", "rx_packets", use_negative=False)
|
| 238 |
yield "" |
| 239 |
|
| 240 |
@classmethod |
| 241 |
def _get_events_config(cls, label=None, siblings=None): |
| 242 |
if label: |
| 243 |
yield "graph_title ath9k Station Events {label}".format(label=label)
|
| 244 |
else: |
| 245 |
yield "graph_title ath9k Station Events" |
| 246 |
yield "graph_vlabel events per ${graph_period}"
|
| 247 |
yield "graph_category wireless" |
| 248 |
events = set() |
| 249 |
for station in siblings: |
| 250 |
for event in STATION_EVENT_COUNTERS: |
| 251 |
events.add(event) |
| 252 |
for event in events: |
| 253 |
yield "{event}.label {event}".format(event=event)
|
| 254 |
yield "{event}.type COUNTER".format(event=event)
|
| 255 |
yield "" |
| 256 |
|
| 257 |
@classmethod |
| 258 |
def _get_rate_fieldname(cls, rate): |
| 259 |
return "rate_{0}".format(rate.lower()).replace(".", "_")
|
| 260 |
|
| 261 |
@classmethod |
| 262 |
def _get_rc_stats_config(cls, label=None, siblings=None): |
| 263 |
if label: |
| 264 |
yield "graph_title ath9k Station Transmit Rates {label} Success".format(label=label)
|
| 265 |
else: |
| 266 |
yield "graph_title ath9k Station Transmit Rates Success" |
| 267 |
yield "graph_vlabel transmit rates %" |
| 268 |
yield "graph_category wireless" |
| 269 |
yield "graph_args --base 1000 -r --lower-limit 0 --upper-limit 100" |
| 270 |
all_rates = {}
|
| 271 |
# collect alle unique rates |
| 272 |
for station in siblings: |
| 273 |
for rate, details in station._rc_stats.items(): |
| 274 |
all_rates[rate] = details |
| 275 |
# return all rates |
| 276 |
is_first = True |
| 277 |
num_extract = lambda text: int("".join([char for char in text if "0" <= char <= "9"]))
|
| 278 |
get_key = lambda rate_name: cls._get_rate_fieldname(all_rates[rate_name]["rate"]) |
| 279 |
# add all rates for percent visualization ("MCS7,MCS6,MCS5,MCS4,MCS3,MCS2,MCS1,MCS0,+,+,+,+,+,+,+")
|
| 280 |
cdef = None |
| 281 |
for sum_rate in all_rates: |
| 282 |
if cdef is None: |
| 283 |
cdef = get_key(sum_rate) |
| 284 |
else: |
| 285 |
cdef = "{key},{cdef},+".format(key=get_key(sum_rate), cdef=cdef)
|
| 286 |
yield "sum.label Sum of all counters" |
| 287 |
yield "sum.type DERIVE" |
| 288 |
yield "sum.graph no" |
| 289 |
for index, rate in enumerate(sorted(all_rates, key=num_extract)): |
| 290 |
details = all_rates[rate] |
| 291 |
key = get_key(rate) |
| 292 |
yield "{key}.label {rate_label}".format(key=key, rate_label=details["rate_label"])
|
| 293 |
yield "{key}.type DERIVE".format(key=key)
|
| 294 |
yield "{key}.min 0".format(key=key)
|
| 295 |
if index < len(QUALITY_GRAPH_COLORS_16): |
| 296 |
yield "{key}.colour {colour}".format(key=key, colour=QUALITY_GRAPH_COLORS_16[index])
|
| 297 |
yield "{key}.draw {draw_type}".format(key=key, draw_type=("AREA" if is_first else "STACK"))
|
| 298 |
# divide the current value by the above sum of all counters and calculate percent |
| 299 |
yield "{key}.cdef 100,{key},sum,/,*".format(key=key, cdef=cdef)
|
| 300 |
is_first = False |
| 301 |
yield "" |
| 302 |
|
| 303 |
|
| 304 |
class WifiInterface: |
| 305 |
|
| 306 |
def __init__(self, name, path, graph_base): |
| 307 |
self._path = path |
| 308 |
self._graph_base = graph_base |
| 309 |
self.name = name |
| 310 |
self.stations = tuple(self._parse_stations()) |
| 311 |
|
| 312 |
def _parse_arp_cache(self): |
| 313 |
""" read IPs and MACs from /proc/net/arp and return a dictionary for MAC -> IP """ |
| 314 |
arp_cache = {}
|
| 315 |
# example content: |
| 316 |
# IP address HW type Flags HW address Mask Device |
| 317 |
# 192.168.2.70 0x1 0x0 00:00:00:00:00:00 * eth0.10 |
| 318 |
# 192.168.12.76 0x1 0x2 24:a4:3c:fd:76:98 * eth1.10 |
| 319 |
for line in open("/proc/net/arp", "r").read().split("\n"):
|
| 320 |
# skip empty lines |
| 321 |
if not line: continue |
| 322 |
tokens = line.split() |
| 323 |
ip, mac = tokens[0], tokens[3] |
| 324 |
# the header line can be ignored - all other should have well-formed MACs |
| 325 |
if not ":" in mac: continue |
| 326 |
# ignore remote peers outside of the broadcast domain |
| 327 |
if mac == "00:00:00:00:00:00": continue |
| 328 |
arp_cache[mac] = ip |
| 329 |
return arp_cache |
| 330 |
|
| 331 |
def _parse_stations(self): |
| 332 |
stations_base = os.path.join(self._path, "stations") |
| 333 |
arp_cache = self._parse_arp_cache() |
| 334 |
for item in os.listdir(stations_base): |
| 335 |
peer_mac = item |
| 336 |
# use the IP or fall back to the MAC without separators (":")
|
| 337 |
if peer_mac in arp_cache: |
| 338 |
label = arp_cache[peer_mac] |
| 339 |
key = peer_mac.replace(":", "")
|
| 340 |
else: |
| 341 |
label = peer_mac |
| 342 |
key = "host_" + peer_mac.replace(":", "").replace(".", "")
|
| 343 |
yield Station(label, key, os.path.join(stations_base, item)) |
| 344 |
|
| 345 |
def get_config(self, scope): |
| 346 |
yield from Station.get_summary_config(scope, self.stations, self._graph_base) |
| 347 |
for station in self.stations: |
| 348 |
yield from station.get_config(scope, self._graph_base) |
| 349 |
yield "" |
| 350 |
|
| 351 |
def get_values(self, scope): |
| 352 |
yield from Station.get_summary_values(scope, self.stations, self._graph_base) |
| 353 |
for station in self.stations: |
| 354 |
yield from station.get_values(scope, self._graph_base) |
| 355 |
yield "" |
| 356 |
|
| 357 |
|
| 358 |
class Ath9kDriver: |
| 359 |
|
| 360 |
def __init__(self, path, graph_base): |
| 361 |
self._path = path |
| 362 |
self._graph_base = graph_base |
| 363 |
self.interfaces = tuple(self._parse_interfaces()) |
| 364 |
|
| 365 |
def _parse_interfaces(self): |
| 366 |
for phy in os.listdir(self._path): |
| 367 |
phy_path = os.path.join(self._path, phy) |
| 368 |
for item in os.listdir(phy_path): |
| 369 |
if item.startswith("netdev:"):
|
| 370 |
wifi = item.split(":", 1)[1]
|
| 371 |
label = "{phy}/{interface}".format(phy=phy, interface=wifi)
|
| 372 |
wifi_path = os.path.join(phy_path, item) |
| 373 |
graph_base = "{base}_{phy}_{interface}".format(base=self._graph_base, phy=phy, interface=wifi)
|
| 374 |
yield WifiInterface(label, wifi_path, graph_base) |
| 375 |
|
| 376 |
def get_config(self, scope): |
| 377 |
for interface in self.interfaces: |
| 378 |
yield from interface.get_config(scope) |
| 379 |
|
| 380 |
def get_values(self, scope): |
| 381 |
for interface in self.interfaces: |
| 382 |
yield from interface.get_values(scope) |
| 383 |
|
| 384 |
|
| 385 |
|
| 386 |
def _get_up_down_pair(unit, key_up, key_down, factor=None, divider=None, use_negative=True): |
| 387 |
""" return all required statements for a munin-specific up/down value pair |
| 388 |
"factor" or "divider" can be given for unit conversions |
| 389 |
""" |
| 390 |
for key in (key_up, key_down): |
| 391 |
if use_negative: |
| 392 |
yield "{key}.label {unit}".format(key=key, unit=unit)
|
| 393 |
else: |
| 394 |
yield "{key}.label {key} {unit}".format(key=key, unit=unit)
|
| 395 |
yield "{key}.type COUNTER".format(key=key)
|
| 396 |
if factor: |
| 397 |
yield "{key}.cdef {key},{factor},*".format(key=key, factor=factor)
|
| 398 |
if divider: |
| 399 |
yield "{key}.cdef {key},{divider},/".format(key=key, divider=divider)
|
| 400 |
if use_negative: |
| 401 |
yield "{key_down}.graph no".format(key_down=key_down)
|
| 402 |
yield "{key_up}.negative {key_down}".format(key_up=key_up, key_down=key_down)
|
| 403 |
|
| 404 |
|
| 405 |
def get_scope(): |
| 406 |
called_name = os.path.basename(sys.argv[0]) |
| 407 |
name_prefix = "ath9k_" |
| 408 |
if called_name.startswith(name_prefix): |
| 409 |
scope = called_name[len(name_prefix):] |
| 410 |
if not scope in PLUGIN_SCOPES: |
| 411 |
print_error("Invalid scope requested: {0} (expected: {1})".format(scope, PLUGIN_SCOPES))
|
| 412 |
sys.exit(2) |
| 413 |
else: |
| 414 |
print_error("Invalid filename - failed to discover plugin scope")
|
| 415 |
sys.exit(2) |
| 416 |
return scope |
| 417 |
|
| 418 |
|
| 419 |
def print_error(message): |
| 420 |
# necessary fallback for micropython |
| 421 |
linesep = getattr(os, "linesep", "\n") |
| 422 |
sys.stderr.write(message + linesep) |
| 423 |
|
| 424 |
|
| 425 |
if __name__ == "__main__": |
| 426 |
ath9k = Ath9kDriver(SYS_BASE_DIR, GRAPH_BASE_NAME) |
| 427 |
# parse arguments |
| 428 |
if len(sys.argv) > 1: |
| 429 |
if sys.argv[1]=="config": |
| 430 |
for item in ath9k.get_config(get_scope()): |
| 431 |
print(item) |
| 432 |
sys.exit(0) |
| 433 |
elif sys.argv[1] == "autoconf": |
| 434 |
if os.path.exists(SYS_BASE_PATH): |
| 435 |
print('yes')
|
| 436 |
else: |
| 437 |
print('no')
|
| 438 |
sys.exit(0) |
| 439 |
elif sys.argv[1] == "suggest": |
| 440 |
for scope in PLUGIN_SCOPES: |
| 441 |
print(scope) |
| 442 |
sys.exit(0) |
| 443 |
elif sys.argv[1] == "version": |
| 444 |
print_error('olsrd Munin plugin, version %s' % plugin_version)
|
| 445 |
sys.exit(0) |
| 446 |
elif sys.argv[1] == "": |
| 447 |
# ignore |
| 448 |
pass |
| 449 |
else: |
| 450 |
# unknown argument |
| 451 |
print_error("Unknown argument")
|
| 452 |
sys.exit(1) |
| 453 |
|
| 454 |
# output values |
| 455 |
for item in ath9k.get_values(get_scope()): |
| 456 |
print(item) |
| 457 |
|
| 458 |
# final marker for shell / python hybrid script (see "Interpreter Selection") |
| 459 |
EOF = True |
| 460 |
EOF |
