1#!/usr/bin/env python 2# 3# Copyright (c) 2016, The OpenThread Authors. 4# All rights reserved. 5# 6# Redistribution and use in source and binary forms, with or without 7# modification, are permitted provided that the following conditions are met: 8# 1. Redistributions of source code must retain the above copyright 9# notice, this list of conditions and the following disclaimer. 10# 2. Redistributions in binary form must reproduce the above copyright 11# notice, this list of conditions and the following disclaimer in the 12# documentation and/or other materials provided with the distribution. 13# 3. Neither the name of the copyright holder nor the 14# names of its contributors may be used to endorse or promote products 15# derived from this software without specific prior written permission. 16# 17# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27# POSSIBILITY OF SUCH DAMAGE. 28# 29 30import logging 31import os 32import re 33import telnetlib 34import time 35 36try: 37 # python 2 38 from urllib2 import HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_opener 39except ImportError: 40 # python 3 41 from urllib.request import HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_opener 42 43logger = logging.getLogger(__name__) 44 45try: 46 from pysnmp.hlapi import SnmpEngine, CommunityData, UdpTransportTarget, ContextData, getCmd, setCmd, ObjectType, ObjectIdentity, Integer32 47except ImportError: 48 logger.warning('PySNMP module is not installed. Install if EATON_PDU_CONTROLLER is used') 49 50 51class PduController(object): 52 53 def open(self, **params): 54 """Open PDU controller connection""" 55 raise NotImplementedError 56 57 def reboot(self, **params): 58 """Reboot an outlet or a board passed as params""" 59 raise NotImplementedError 60 61 def close(self): 62 """Close PDU controller connection""" 63 raise NotImplementedError 64 65 66class DummyPduController(PduController): 67 """Dummy implementation which only says that PDU controller is not connected""" 68 69 def open(self, **params): 70 pass 71 72 def reboot(self, **params): 73 logger.info('No PDU controller connected.') 74 75 def close(self): 76 pass 77 78 79class ApcPduController(PduController): 80 81 def __init__(self): 82 self.tn = None 83 84 def __del__(self): 85 self.close() 86 87 def _init(self): 88 """Initialize the telnet connection 89 """ 90 self.tn = telnetlib.Telnet(self.ip, self.port) 91 self.tn.read_until('User Name') 92 self.tn.write('apc\r\n') 93 self.tn.read_until('Password') 94 self.tn.write('apc\r\n') 95 self.until_done() 96 97 def open(self, **params): 98 """Open telnet connection 99 100 Args: 101 params (dict), must contain two parameters "ip" - ip address or hostname and "port" - port number 102 103 Example: 104 params = {'port': 23, 'ip': 'localhost'} 105 """ 106 logger.info('opening telnet') 107 self.port = params['port'] 108 self.ip = params['ip'] 109 self.tn = None 110 self._init() 111 112 def close(self): 113 """Close telnet connection""" 114 logger.info('closing telnet') 115 if self.tn: 116 self.tn.close() 117 118 def until_done(self): 119 """Wait until the prompt encountered 120 """ 121 self.until(r'^>') 122 123 def until(self, regex): 124 """Wait until the regex encountered 125 """ 126 logger.debug('waiting for %s', regex) 127 r = re.compile(regex, re.M) 128 self.tn.expect([r]) 129 130 def reboot(self, **params): 131 """Reboot outlet 132 133 Args: 134 params (dict), must contain parameter "outlet" - outlet number 135 136 Example: 137 params = {'outlet': 1} 138 """ 139 outlet = params['outlet'] 140 141 # main menu 142 self.tn.write('\x1b\r\n') 143 self.until_done() 144 # Device Manager 145 self.tn.write('1\r\n') 146 self.until_done() 147 # Outlet Management 148 self.tn.write('2\r\n') 149 self.until_done() 150 # Outlet Control 151 self.tn.write('1\r\n') 152 self.until_done() 153 # Select outlet 154 self.tn.write('%d\r\n' % outlet) 155 self.until_done() 156 # Control 157 self.tn.write('1\r\n') 158 self.until_done() 159 # off 160 self.tn.write('2\r\n') 161 self.until('to cancel') 162 self.tn.write('YES\r\n') 163 self.until('to continue') 164 self.tn.write('\r\n') 165 self.until_done() 166 167 time.sleep(5) 168 # on 169 self.tn.write('1\r\n') 170 self.until('to cancel') 171 self.tn.write('YES\r\n') 172 self.until('to continue') 173 self.tn.write('\r\n') 174 self.until_done() 175 176 177class NordicBoardPduController(PduController): 178 179 def open(self, **params): 180 pass 181 182 def _pin_reset(self, serial_number): 183 os.system('nrfjprog -f NRF52 --snr {} -p'.format(serial_number)) 184 185 def reboot(self, **params): 186 boards_serial_numbers = params['boards_serial_numbers'] 187 188 for serial_number in boards_serial_numbers: 189 logger.info('Resetting board with the serial number: %s', serial_number) 190 self._pin_reset(serial_number) 191 192 def close(self): 193 pass 194 195 196class EatonPduController(PduController): 197 198 outlet_oid_cmd_get_state_base = '1.3.6.1.4.1.534.6.6.7.6.6.1.2.0.' 199 outlet_oid_cmd_set_on_base = '1.3.6.1.4.1.534.6.6.7.6.6.1.4.0.' 200 outlet_oid_cmd_set_off_base = '1.3.6.1.4.1.534.6.6.7.6.6.1.3.0.' 201 outlet_oid_cmd_reboot_base = '1.3.6.1.4.1.534.6.6.7.6.6.1.5.0.' 202 outlet_oid_cmd_set_reboot_delay_seconds_base = '1.3.6.1.4.1.534.6.6.7.6.6.1.8.0.' 203 204 PDU_COMMAND_TIMEOUT = 5 205 206 def open(self, **params): 207 missing_fields = ['ip', 'port'] 208 missing_fields = [field for field in missing_fields if field not in params.keys()] 209 if missing_fields: 210 raise KeyError('Missing keys in PDU params: {}'.format(missing_fields)) 211 self.params = params 212 self.type = 'pdu' 213 self.ip = self.params['ip'] 214 self.snmp_agent_port = int(self.params['port']) 215 self._community = 'public' 216 self._snmp_engine = SnmpEngine() 217 self._community_data = CommunityData(self._community, mpModel=0) 218 self._udp_transport_target = UdpTransportTarget((self.ip, self.snmp_agent_port)) 219 self._context = ContextData() 220 221 def _outlet_oid_get(self, param, socket): 222 """ 223 Translates command to the OID number representing a command for the specific power socket. 224 225 Args: 226 param (str), command string 227 socket (int), socket index 228 229 Return: 230 full OID identifying the SNMP object (str) 231 """ 232 parameters = { 233 'get_state': self.outlet_oid_cmd_get_state_base, 234 'set_on': self.outlet_oid_cmd_set_on_base, 235 'set_off': self.outlet_oid_cmd_set_off_base, 236 'set_reboot_delay': self.outlet_oid_cmd_set_reboot_delay_seconds_base, 237 'reboot': self.outlet_oid_cmd_reboot_base 238 } 239 240 return parameters[param.lower()] + str(socket) 241 242 # Performs set command to specific OID with a given value by sending a SNMP Set message. 243 def _oid_set(self, oid, value): 244 """ 245 Performs set command to specific OID with a given value by sending a SNMP Set message. 246 247 Args: 248 oid (str): Full OID identifying the object to be read. 249 value (int): Value to be written to the OID as Integer32. 250 """ 251 errorIndication, errorStatus, errorIndex, varBinds = next( 252 setCmd(self._snmp_engine, self._community_data, self._udp_transport_target, self._context, 253 ObjectType(ObjectIdentity(oid), Integer32(value)))) 254 255 if errorIndication: 256 msg = 'Found PDU errorIndication: {}'.format(errorIndication) 257 logger.exception(msg) 258 raise RuntimeError(msg) 259 elif errorStatus: 260 msg = 'Found PDU errorStatus: {}'.format(errorStatus) 261 logger.exception(msg) 262 raise RuntimeError(msg) 263 264 def _oid_get(self, oid): 265 """ 266 Performs SNMP get command and returns OID value by sending a SNMP Get message. 267 268 Args: 269 oid (str): Full OID identifying the object to be read. 270 271 Return: 272 OID value (int) 273 """ 274 errorIndication, errorStatus, errorIndex, varBinds = next( 275 getCmd(self._snmp_engine, self._community_data, self._udp_transport_target, self._context, 276 ObjectType(ObjectIdentity(oid)))) 277 278 if errorIndication: 279 msg = 'Found PDU errorIndication: {}'.format(errorIndication) 280 logger.exception(msg) 281 raise RuntimeError(msg) 282 elif errorStatus: 283 msg = 'Found PDU errorStatus: {}'.format(errorStatus) 284 logger.exception(msg) 285 raise RuntimeError(msg) 286 287 return int(str(varBinds[-1]).partition('= ')[-1]) 288 289 def _outlet_value_set(self, cmd, socket, value=1): 290 """ 291 Sets outlet parameter value. 292 293 Args: 294 cmd (str): OID base 295 socket (int): socket index (last OID number) 296 value (int): value to be set 297 """ 298 oid = self._outlet_oid_get(cmd, socket) 299 300 # Values other than 1 does not make sense with commands other than "set_reboot_delay". 301 if cmd != 'set_reboot_delay': 302 value = 1 303 304 self._oid_set(oid, value) 305 306 def _outlet_value_get(self, cmd, socket): 307 """ 308 Read outlet parameter value. 309 310 Args: 311 cmd (str): OID base 312 socket (int): socket index (last OID number) 313 314 Return: 315 parameter value (int) 316 """ 317 oid = self._outlet_oid_get(cmd, socket) 318 319 return self._oid_get(oid) 320 321 def validate_state(self, socket, state): 322 return (self._outlet_value_get('get_state', socket) == state) 323 324 def turn_off(self, sockets): 325 """ 326 Turns the specified socket off. 327 328 Args: 329 sockets (list(int)): sockets to be turned off 330 """ 331 logger.info('Executing turn OFF for: {}'.format(sockets)) 332 333 for socket in sockets: 334 self._outlet_value_set('set_off', socket) 335 time.sleep(2) 336 337 timeout = time.time() + self.PDU_COMMAND_TIMEOUT 338 while ((time.time() < timeout) and not self.validate_state(socket, 0)): 339 time.sleep(0.1) 340 341 if self.validate_state(socket, 0): 342 logger.debug('Turned OFF socket {} at {}'.format(socket, self.ip)) 343 else: 344 logger.error('Failed to turn OFF socket {} at {}'.format(socket, self.ip)) 345 346 def turn_on(self, sockets): 347 """ 348 Turns the specified socket on. 349 350 Args: 351 sockets (list(int)): sockets to be turned on 352 """ 353 354 logger.info('Executing turn ON for: {}'.format(sockets)) 355 356 for socket in sockets: 357 self._outlet_value_set('set_on', socket) 358 time.sleep(2) 359 360 timeout = time.time() + self.PDU_COMMAND_TIMEOUT 361 while ((time.time() < timeout) and not self.validate_state(socket, 1)): 362 time.sleep(0.1) 363 364 if self.validate_state(socket, 1): 365 logger.debug('Turned ON socket {} at {}'.format(socket, self.ip)) 366 else: 367 logger.error('Failed to turn ON socket {} at {}'.format(socket, self.ip)) 368 369 def close(self): 370 self._community = None 371 self._snmp_engine = None 372 self._community_data = None 373 self._udp_transport_target = None 374 self._context = None 375 376 def reboot(self, **params): 377 """ 378 Reboots the sockets specified in the constructor with off and on delays. 379 380 Args: 381 sockets (list(int)): sockets to reboot 382 """ 383 384 logger.info('Executing power cycle for: {}'.format(params['sockets'])) 385 self.turn_off(params['sockets']) 386 time.sleep(10) 387 self.turn_on(params['sockets']) 388 time.sleep(5) 389 390 391class IpPowerSocketPduController(PduController): 392 393 def open(self, **params): 394 self._base_url = 'http://{}/outs.cgi?out'.format(params['ip']) 395 password_manager = HTTPPasswordMgrWithDefaultRealm() 396 password_manager.add_password(None, self._base_url, params['user'], params['pass']) 397 authentication_handler = HTTPBasicAuthHandler(password_manager) 398 self._opener = build_opener(authentication_handler) 399 400 def reboot(self, **params): 401 logger.info('Executing power cycle') 402 for socket in params['sockets']: 403 self._turn_off(socket) 404 time.sleep(2) 405 self._turn_on(socket) 406 time.sleep(2) 407 408 def _change_state(self, socket, state): 409 self._opener.open('{}{}={}'.format(self._base_url, socket, state)) 410 411 def _turn_off(self, socket): 412 self._change_state(socket, 1) 413 414 def _turn_on(self, socket): 415 self._change_state(socket, 0) 416 417 def close(self): 418 self._base_url = None 419 self._opener = None 420 421 422class ManualPduController(PduController): 423 424 def open(self, **kwargs): 425 pass 426 427 def reboot(self, **kwargs): 428 input('Reset all devices and press enter to continue..') 429 430 def close(self): 431 pass 432