├── env-setup.sh ├── lib └── netconify │ ├── constants.py │ ├── __init__.py │ ├── tty_telnet.py │ ├── tty_serial.py │ ├── facts.py │ ├── tty_ssh.py │ ├── tty_netconf.py │ ├── tty.py │ └── cmdo.py ├── tools └── netconify ├── .gitignore ├── COPYRIGHT ├── setup.py ├── README.md └── LICENSE /env-setup.sh: -------------------------------------------------------------------------------- 1 | export PYTHONPATH=`pwd`/lib:$PYTHONPATH 2 | export PATH=$PATH:`pwd`/tools 3 | -------------------------------------------------------------------------------- /lib/netconify/constants.py: -------------------------------------------------------------------------------- 1 | version = "1.0.3" 2 | date = "2016-March-17" 3 | author = "Jeremy Schulman, @nwkautomaniac" 4 | -------------------------------------------------------------------------------- /tools/netconify: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | from netconify.cmdo import * 5 | 6 | nc = netconifyCmdo() 7 | results = nc.run() 8 | 9 | if results['failed'] is True: 10 | print results['errmsg'] 11 | sys.exit(1) 12 | -------------------------------------------------------------------------------- /lib/netconify/__init__.py: -------------------------------------------------------------------------------- 1 | from netconify.tty_serial import Serial 2 | from netconify.tty_telnet import Telnet 3 | from netconify.tty_ssh import SecureShell 4 | from netconify import constants as C 5 | 6 | __version__ = C.version 7 | __date__ = C.date 8 | __author__ = C.author 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | tests 10 | dist 11 | build 12 | eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | # Copyright (c): 2014 2 | # Author: Jeremy Schulman, Juniper Networks, Inc. 3 | # All Rights Reserved 4 | # License: Apache 2.0 (see LICENSE file) 5 | # 6 | # YOU MUST ACCEPT THE TERMS OF THIS DISCLAIMER TO USE THIS SOFTWARE, 7 | # IN ADDITION TO ANY OTHER LICENSES AND TERMS REQUIRED BY THE LICENSE. 8 | # 9 | # THE AUTHOR IS WILLING TO MAKE THE INCLUDED SOFTWARE AVAILABLE TO YOU ONLY 10 | # UPON THE CONDITION THAT YOU ACCEPT ALL OF THE TERMS CONTAINED IN 11 | # THIS DISCLAIMER. PLEASE READ THE TERMS AND CONDITIONS OF THIS DISCLAIMER 12 | # CAREFULLY. 13 | # 14 | # THE SOFTWARE CONTAINED IN THIS FILE IS PROVIDED "AS IS." THE AUTHOR MAKES NO 15 | # WARRANTIES OF ANY KIND WHATSOEVER WITH RESPECT TO SOFTWARE. ALL EXPRESS OR 16 | # IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING ANY WARRANTY 17 | # OF NON-INFRINGEMENT OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR A 18 | # PARTICULAR PURPOSE, ARE HEREBY DISCLAIMED AND EXCLUDED TO THE EXTENT 19 | # ALLOWED BY APPLICABLE LAW. 20 | # 21 | # IN NO EVENT WILL THE AUTHOR BE LIABLE FOR ANY DIRECT OR INDIRECT DAMAGES, 22 | # INCLUDING BUT NOT LIMITED TO LOST REVENUE, PROFIT OR DATA, OR 23 | # FOR DIRECT, SPECIAL, INDIRECT, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES 24 | # HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY ARISING OUT OF THE 25 | # USE OF OR INABILITY TO USE THE SOFTWARE, EVEN IF THE AUTHOR HAS BEEN ADVISED 26 | # OF THE POSSIBILITY OF SUCH DAMAGES. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from glob import glob 4 | from setuptools import setup, find_packages 5 | 6 | requirements = ['pyserial', 'lxml', 'paramiko'] 7 | 8 | setup( 9 | name="junos-netconify", 10 | version="1.0.3", 11 | author="Jeremy Schulman", 12 | author_email="jnpr-community-netdev@juniper.net", 13 | description=("Junos console/bootstrap automation"), 14 | license="Apache 2.0", 15 | keywords="Junos NETCONF basic CONSOLE automation", 16 | url="http://www.github.com/Juniper/py-junos-netconify", 17 | install_requires=requirements, 18 | packages=find_packages('lib'), 19 | package_dir={'': 'lib'}, 20 | scripts=['tools/netconify'], 21 | classifiers=[ 22 | 'Development Status :: 5 - Production/Stable', 23 | 'Environment :: Console', 24 | 'Intended Audience :: Developers', 25 | 'Intended Audience :: Information Technology', 26 | 'Intended Audience :: System Administrators', 27 | 'Intended Audience :: Telecommunications Industry', 28 | 'License :: OSI Approved :: Apache Software License', 29 | 'Operating System :: OS Independent', 30 | 'Programming Language :: Other Scripting Engines', 31 | 'Programming Language :: Python', 32 | 'Programming Language :: Python :: 2.6', 33 | 'Programming Language :: Python :: 2.7', 34 | 'Topic :: Software Development :: Libraries', 35 | 'Topic :: Software Development :: Libraries :: Application Frameworks', 36 | 'Topic :: Software Development :: Libraries :: Python Modules', 37 | 'Topic :: System :: Networking', 38 | 'Topic :: Text Processing :: Markup :: XML' 39 | ], 40 | ) 41 | -------------------------------------------------------------------------------- /lib/netconify/tty_telnet.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | import telnetlib 3 | 4 | from .tty import Terminal 5 | 6 | # ------------------------------------------------------------------------- 7 | # Terminal connection over TELNET CONSOLE 8 | # ------------------------------------------------------------------------- 9 | 10 | 11 | class Telnet(Terminal): 12 | RETRY_OPEN = 3 # number of attempts to open TTY 13 | RETRY_BACKOFF = 2 # seconds to wait between retries 14 | 15 | def __init__(self, host, port, **kvargs): 16 | """ 17 | :host: 18 | The hostname or ip-addr of the ternminal server 19 | 20 | :port: 21 | The TCP port that maps to the TTY device on the 22 | console server 23 | 24 | :kvargs['timeout']: 25 | this is the tty read polling timeout. 26 | generally you should not have to tweak this. 27 | """ 28 | # initialize the underlying TTY device 29 | 30 | self._tn = telnetlib.Telnet() 31 | self.host = host 32 | self.port = port 33 | self.timeout = kvargs.get('timeout', self.TIMEOUT) 34 | self._tty_name = "{0}:{1}".format(host, port) 35 | 36 | Terminal.__init__(self, **kvargs) 37 | 38 | # ------------------------------------------------------------------------- 39 | # I/O open close called from Terminal class 40 | # ------------------------------------------------------------------------- 41 | 42 | def _tty_open(self): 43 | retry = self.RETRY_OPEN 44 | while retry > 0: 45 | try: 46 | self._tn.open(self.host, self.port, self.timeout) 47 | break 48 | except Exception as err: 49 | retry -= 1 50 | self.notify("TTY busy", "checking back in {0} ...".format(self.RETRY_BACKOFF)) 51 | sleep(self.RETRY_BACKOFF) 52 | else: 53 | raise RuntimeError("open_fail: port not ready") 54 | 55 | self.write('\n') 56 | 57 | def _tty_close(self): 58 | self._tn.close() 59 | 60 | # ------------------------------------------------------------------------- 61 | # I/O read and write called from Terminal class 62 | # ------------------------------------------------------------------------- 63 | 64 | def write(self, content): 65 | """ write content + """ 66 | self._tn.write(content + '\n') 67 | 68 | def rawwrite(self, content): 69 | """ write content as-is """ 70 | self._tn.write(content) 71 | 72 | def read(self): 73 | """ read a single line """ 74 | return self._tn.read_until('\n', self.EXPECT_TIMEOUT) 75 | 76 | def read_prompt(self): 77 | got = self._tn.expect(Terminal._RE_PAT, self.EXPECT_TIMEOUT) 78 | sre = got[1] 79 | 80 | if 'in use' in got[2]: 81 | raise RuntimeError("open_fail: port already in use") 82 | 83 | # (buffer, RE group) 84 | return (None, None) if not got[1] else (got[2], got[1].lastgroup) 85 | -------------------------------------------------------------------------------- /lib/netconify/tty_serial.py: -------------------------------------------------------------------------------- 1 | import serial 2 | import re 3 | from time import sleep 4 | from datetime import datetime, timedelta 5 | 6 | from .tty import Terminal 7 | 8 | # ------------------------------------------------------------------------- 9 | # Terminal connection over SERIAL CONSOLE 10 | # ------------------------------------------------------------------------- 11 | 12 | _PROMPT = re.compile('|'.join(Terminal._RE_PAT)) 13 | 14 | 15 | class Serial(Terminal): 16 | 17 | def __init__(self, port='/dev/ttyUSB0', **kvargs): 18 | """ 19 | :port: 20 | the serial port, defaults to USB0 since this 21 | 22 | :kvargs['timeout']: 23 | this is the tty read polling timeout. 24 | generally you should not have to tweak this. 25 | """ 26 | # initialize the underlying TTY device 27 | 28 | self.port = port 29 | self._ser = serial.Serial() 30 | self._ser.port = port 31 | self._ser.timeout = kvargs.get('timeout', self.TIMEOUT) 32 | 33 | self._tty_name = self.port 34 | 35 | Terminal.__init__(self, **kvargs) 36 | 37 | # ------------------------------------------------------------------------- 38 | # I/O open close called from Terminal class 39 | # ------------------------------------------------------------------------- 40 | 41 | def _tty_open(self): 42 | try: 43 | self._ser.open() 44 | except OSError as err: 45 | raise RuntimeError("open_failed:{0}".format(err.strerror)) 46 | self.write('\n\n\n') # hit a few times, yo! 47 | 48 | def _tty_close(self): 49 | self._ser.flush() 50 | self._ser.close() 51 | 52 | # ------------------------------------------------------------------------- 53 | # I/O read and write called from Terminal class 54 | # ------------------------------------------------------------------------- 55 | 56 | def write(self, content): 57 | """ write content + """ 58 | self._ser.write(content + '\n') 59 | self._ser.flush() 60 | 61 | def rawwrite(self, content): 62 | self._ser.write(content) 63 | 64 | def read(self): 65 | """ read a single line """ 66 | return self._ser.readline() 67 | 68 | def read_prompt(self): 69 | """ 70 | reads text from the serial console (using readline) until 71 | a match is found against the :expect: regular-expression object. 72 | When a match is found, return a tuple(,) where 73 | is the complete text and is the name of the 74 | regular-expression group. If a timeout occurs, then return 75 | the tuple(None,None). 76 | """ 77 | rxb = '' 78 | mark_start = datetime.now() 79 | mark_end = mark_start + timedelta(seconds=self.EXPECT_TIMEOUT) 80 | 81 | while datetime.now() < mark_end: 82 | sleep(0.1) # do not remove 83 | line = self._ser.readline() 84 | if not line: 85 | continue 86 | rxb += line 87 | found = _PROMPT.search(rxb) 88 | if found is not None: 89 | break # done reading 90 | else: 91 | # exceeded the while loop timeout 92 | return (None, None) 93 | 94 | return (rxb, found.lastgroup) 95 | -------------------------------------------------------------------------------- /lib/netconify/facts.py: -------------------------------------------------------------------------------- 1 | import re 2 | from lxml.builder import E 3 | from lxml import etree 4 | 5 | 6 | class Facts(object): 7 | 8 | def __init__(self, parent): 9 | self.rpc = parent.rpc 10 | self.facts = {} 11 | 12 | @property 13 | def items(self): 14 | return self.facts 15 | 16 | def version(self): 17 | rsp = self.rpc('get-software-information') 18 | self.swinfo = rsp # keep this since we may want it later 19 | 20 | # extract the version 21 | # First try the tag present in >= 15.1 22 | swinfo = rsp.findtext('junos-version', default=None) 23 | if not swinfo: 24 | # For < 15.1, get version from the "junos" package. 25 | pkginfo = rsp.xpath( 26 | './/package-information[normalize-space(name)="junos"]/comment' 27 | )[0].text 28 | try: 29 | swinfo = re.findall(r'\[(.*)\]', pkginfo)[0] 30 | except: 31 | swinfo = "0.0I0.0" 32 | self.facts['version'] = swinfo 33 | 34 | # extract the host-name 35 | self.facts['hostname'] = rsp.xpath('.//host-name')[0].text 36 | 37 | # extract the product model/models 38 | product_model = rsp.xpath('//product-model') 39 | num_models = len(product_model) 40 | if num_models == 0: 41 | self.facts['model'] = None 42 | elif num_models == 1: 43 | self.facts['model'] = product_model[0].text.upper() 44 | else: 45 | fpc = lambda m: m.xpath('../../re-name')[0].text 46 | self.facts['models'] = dict((fpc(m), m.text.upper()) for m in product_model) 47 | 48 | def chassis(self): 49 | try: 50 | # try to get the chassis inventory. this will fail if the device 51 | # happens to be a QFX in 'node' mode, so use exception handling 52 | rsp = self.rpc('get-chassis-inventory') 53 | # keep this since we want to save the data to file 54 | self.inventory = rsp 55 | chas = rsp.find('chassis') 56 | sn = chas.findtext('serial-number') 57 | self.facts['model'] = chas.findtext('description').upper() 58 | 59 | # use the chassis level serial number, and if that doesn't exist 60 | # look for the 'Backplane' serial number 61 | self.facts['serialnumber'] = sn if sn is not None else \ 62 | chas.xpath( 63 | 'chassis-module[name="Backplane"]/serial-number')[0].text 64 | except: 65 | # basically this catches the case if the device is a QFX in node mode; 66 | # the chassis-subsystem isn't running. the hostname is the serial 67 | # nubmer 68 | self.facts['serialnumber'] = self.facts['hostname'] 69 | pass 70 | 71 | def eth(self, ifname): 72 | cmd = E('get-interface-information', 73 | E.media(), 74 | E('interface-name', ifname)) 75 | 76 | rsp = self.rpc(etree.tostring(cmd))[0] # at physical-interface 77 | facts = self.facts 78 | 79 | facts[ifname] = {} 80 | facts[ifname]['macaddr'] = rsp.findtext('.//current-physical-address') 81 | facts[ifname]['ifindex'] = rsp.findtext('snmp-index') 82 | facts[ifname]['oper'] = rsp.findtext('oper-status') 83 | facts[ifname]['admin'] = rsp.findtext('admin-status') 84 | facts[ifname]['speed'] = rsp.findtext('speed') 85 | facts[ifname]['duplex'] = rsp.findtext('duplex') 86 | 87 | return facts[ifname] 88 | 89 | def gather(self): 90 | self.version() 91 | self.chassis() 92 | -------------------------------------------------------------------------------- /lib/netconify/tty_ssh.py: -------------------------------------------------------------------------------- 1 | from select import select 2 | import paramiko 3 | import re 4 | import logging 5 | from time import sleep, time 6 | from .tty import Terminal 7 | 8 | _PROMPT = re.compile('|'.join(Terminal._RE_PAT)) 9 | 10 | 11 | class SecureShell(Terminal): 12 | RETRY_BACKOFF = 2 # seconds to wait between retries 13 | SSH_LOGIN_RETRY = 1 # number off ssh login retry to console server 14 | SELECT_WAIT = 0.1 15 | RECVSZ = 1024 16 | 17 | def __init__(self, host, port, s_user, s_passwd, **kvargs): 18 | """ 19 | Utility Constructor 20 | """ 21 | self._ssh = paramiko.SSHClient() 22 | self._ssh.load_system_host_keys() 23 | self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 24 | self._logger = logging.getLogger('paramiko.transport') 25 | self._logger.disabled = True 26 | self.host = host 27 | self.port = port 28 | self.s_user = s_user 29 | self.s_passwd = s_passwd 30 | self.timeout = kvargs.get('timeout', self.TIMEOUT) 31 | self.attempts = self.SSH_LOGIN_RETRY 32 | self._tty_name = "{0}:{1}:{2}:{3}".format(host, port, s_user, s_passwd) 33 | 34 | Terminal.__init__(self, **kvargs) 35 | 36 | def _tty_open(self): 37 | while True: 38 | try: 39 | self._ssh.connect(hostname=self.host, port=int(self.port), 40 | username=self.s_user, password=self.s_passwd, timeout=self.timeout, allow_agent=False, look_for_keys=False) 41 | break 42 | except paramiko.BadHostKeyException: 43 | self.notify( 44 | "SSH", 45 | "Invalid host key for {0}".format( 46 | self.host)) 47 | except paramiko.AuthenticationException: 48 | self.notify( 49 | "SSH", 50 | "Bad username or password when connecting to {0}".format( 51 | self.host)) 52 | else: 53 | raise RuntimeError("open_fail: ssh port not ready") 54 | 55 | self._chan = self._ssh.invoke_shell() 56 | self.write('\n') 57 | 58 | def write(self, data): 59 | """ write data + """ 60 | self._chan.send(data) 61 | self._chan.send('\n') 62 | 63 | def rawwrite(self, data): 64 | """ write data only""" 65 | self._chan.send(data) 66 | 67 | def read(self): 68 | """ 69 | read a single line 70 | this is and ugly hack to mimic serial and telnet which reads one byte at a time 71 | """ 72 | gotr = [] 73 | while True: 74 | data = self._chan.recv(1) 75 | if data is None or len(data) <= 0: 76 | raise ValueError('Unable to detect device prompt') 77 | elif '\n' in data: 78 | self._prompt = data.split('\n')[0].strip() 79 | break 80 | else: 81 | gotr.append(data) 82 | 83 | self._rt = ''.join(str(s) for s in gotr) 84 | return self._rt 85 | 86 | def _tty_close(self): 87 | """ Close the SSH client channel """ 88 | self._chan.close() 89 | 90 | def read_prompt(self): 91 | got = [] 92 | timeout = time() + 15.0 93 | 94 | while time() < timeout: 95 | sleep(0.1) 96 | rd, wr, err = select([self._chan], [], [], self.SELECT_WAIT) 97 | sleep(0.05) 98 | if rd: 99 | data = self._chan.recv(self.RECVSZ) 100 | got.append(data) 101 | found = _PROMPT.search(data) 102 | if found is not None: 103 | break 104 | else: 105 | # exceeded the while loop timeout 106 | raise RuntimeError( 107 | "Netconify Error: ssh could not find string Login:") 108 | 109 | return (got, found.lastgroup) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The repo is under active development. If you take a clone, you are getting the latest, and perhaps not entirely stable code. 2 | 3 | ## ABOUT 4 | 5 | Junos console/bootstrap New-Out-Of-Box (NOOB) configuration automation. 6 | 7 | There are times when you MUST console into a Junos device to perform the NOOB configuration. Generally this configuration is the bare minimum in takes to: 8 | 9 | * set the root password 10 | * set the host-name 11 | * set the management ipaddr 12 | * enable ssh and optionally NETCONF 13 | 14 | The general use-case: 15 | 16 | Primarily this library is used as a Console driver for the Junos Ansible Modules. 17 | 18 | The `netconify` utility can be used perform configuration by logging into the serial console and pushing a configuration file to the device. 19 | 20 | 21 | ## USAGE 22 | 23 | ```` 24 | 25 | usage: netconify [-h] [--version] [-f JUNOS_CONF_FILE] [--merge] [--qfx-node] 26 | [--qfx-switch] [--zeroize] [--shutdown {poweroff,reboot}] 27 | [--facts] [--srx_cluster REQUEST_SRX_CLUSTER] 28 | [--srx_cluster_disable] [-S [SAVEDIR]] [--no-save] [-p PORT] 29 | [-b BAUD] [-t TELNET] [ -s SSH] [--timeout TIMEOUT] [-u USER] 30 | [-P PASSWD] [-k] [-a ATTEMPTS] 31 | [name] 32 | 33 | positional arguments: 34 | name name of Junos device 35 | 36 | optional arguments: 37 | -h, --help show this help message and exit 38 | --version show program's version number and exit 39 | --verbose VERBOSE increase verbose levevel: 0 = default, 1 = login 40 | debug, 2 = rpc reply debug 41 | 42 | DEVICE options: 43 | -f JUNOS_CONF_FILE, --file JUNOS_CONF_FILE 44 | Junos configuration file 45 | --merge load-merge conf file, default is overwrite 46 | --qfx-node Set QFX device into "node" mode 47 | --qfx-switch Set QFX device into "switch" mode 48 | --zeroize ZEROIZE the device 49 | --shutdown {poweroff,reboot} 50 | SHUTDOWN or REBOOT the device 51 | --facts Gather facts and save them into SAVEDIR 52 | --srx_cluster REQUEST_SRX_CLUSTER 53 | cluster_id,node ... Invoke cluster on SRX device and 54 | reboot 55 | --srx_cluster_disable 56 | Disable cluster mode on SRX device and reboot 57 | 58 | DIRECTORY options: 59 | -S [SAVEDIR], --savedir [SAVEDIR] 60 | Files are saved into this directory, $CWD by default 61 | --no-save Do not save facts and inventory files 62 | 63 | CONSOLE options: 64 | -p PORT, --port PORT serial port device 65 | -b BAUD, --baud BAUD serial port baud rate 66 | -s SSH, --ssh SSH ssh server, ,,, 67 | -t TELNET, --telnet TELNET 68 | terminal server, , 69 | --timeout TIMEOUT TTY connection timeout (s) 70 | 71 | LOGIN options: 72 | -u USER, --user USER login user name, defaults to "root" 73 | -P PASSWD, --passwd PASSWD 74 | login user password, *empty* for NOOB 75 | -k prompt for user password 76 | -a ATTEMPTS, --attempts ATTEMPTS 77 | login attempts before giving up 78 | ```` 79 | 80 | ## EXAMPLE 81 | 82 | Junos NOOB devices can netconified: 83 | 84 | ###Telnet: 85 | ```` 86 | [rsherman@py-junos-netconify bin]$ ./netconify --telnet=host,23 -f host.conf 87 | TTY:login:connecting to TTY:host:23 ... 88 | TTY:login:logging in ... 89 | TTY:login:starting NETCONF 90 | conf:loading into device ... 91 | conf:commit ... please be patient 92 | conf:commit completed. 93 | TTY:logout:logging out ... 94 | ```` 95 | The above example is connecting to the host via telnet on port 23 and loading the configuration file specified. 96 | ###SSH: 97 | ```` 98 | r2600r@r2600r-mbp15 [~]netconify --facts --ssh=console-server,19876,c-user,pass123 -u user --passwd "pass123" 99 | TTY:login:connecting to TTY:console-server:19876:c-user:pass123 ... 100 | TTY:login:logging to device ... 101 | TTY:login: OK ... starting NETCONF 102 | facts:retrieving device facts... 103 | facts:saving: ./cfwj-lab1-0011a-facts.json 104 | inventory:saving: ./cfwj-lab1-0011a-inventory.xml 105 | TTY:logout:logging out ... 106 | ```` 107 | The above example is connecting to the host via ssh on port 19876 and gather device facts. Additonal options such as serial connectivity and device specific functions are identified in Usage. If ssh username and password for console are omited, -u/--passwd will be used instead for both console server authetication and device authetication --ssh=console-server,19876,, -u user --passwd "pass123" 108 | 109 | 110 | ## INSTALLATION 111 | 112 | Installation requires Python 2.6 or 2.7 and associate `pip` tool 113 | 114 | pip install junos-netconify 115 | 116 | Installing from Git is also supported (OS must have git installed). 117 | 118 | To install the latest MASTER code 119 | pip install git+https://github.com/Juniper/py-junos-netconify.git 120 | -or- 121 | To install a specific version, branch, tag, etc. 122 | pip install git+https://github.com/Juniper/py-junos-netconify.git@ 123 | 124 | ## UPGRADE 125 | 126 | Upgrading has the same requirements as installation and has the same format with the addition of -UPGRADE 127 | 128 | pip install -U junos-netconify 129 | 130 | ## DEPENDENCIES 131 | 132 | This has been tested with Python 2.6 and 2.7. The required modules are defined in `setup.py`. 133 | 134 | ## LICENSE 135 | 136 | Apache 2.0 137 | 138 | ## CONTRIBUTORS 139 | 140 | - Jeremy Schulman (@nwkautomaniac), Core developer 141 | - Rick Sherman (@shermdog01) 142 | - Patrik Bok (@r2660r) 143 | - Ashley Burston 144 | -------------------------------------------------------------------------------- /lib/netconify/tty_netconf.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | from . import cmdo 4 | from lxml import etree 5 | from lxml.builder import E 6 | from datetime import datetime, timedelta 7 | 8 | 9 | from .facts import Facts 10 | 11 | __all__ = ['xmlmode_netconf'] 12 | 13 | _NETCONF_EOM = ']]>]]>' 14 | _xmlns = re.compile('xmlns=[^>]+') 15 | _xmlns_strip = lambda text: _xmlns.sub('', text) 16 | _junosns = re.compile('junos:') 17 | _junosns_strip = lambda text: _junosns.sub('', text) 18 | 19 | # ========================================================================= 20 | # xmlmode_netconf 21 | # ========================================================================= 22 | 23 | 24 | class tty_netconf(object): 25 | 26 | """ 27 | Basic Junos XML API for bootstraping through the TTY 28 | """ 29 | 30 | def __init__(self, tty): 31 | self._tty = tty 32 | self.hello = None 33 | self.facts = Facts(self) 34 | 35 | # ------------------------------------------------------------------------- 36 | # NETCONF session open and close 37 | # ------------------------------------------------------------------------- 38 | 39 | def open(self, at_shell): 40 | """ start the XML API process and receive the 'hello' message """ 41 | 42 | nc_cmd = ('junoscript', 'xml-mode')[at_shell] 43 | self._tty.write(nc_cmd + ' netconf need-trailer') 44 | 45 | while True: 46 | time.sleep(0.1) 47 | line = self._tty.read() 48 | if line.startswith("