Skip to content

Commit 7147db3

Browse files
Zmegolazbluecmd
authored andcommitted
Added HTTP server. Updated TFTP server to support 32MB+ files. Better Juniper support, it now fetches config on each boot.
1 parent a5db774 commit 7147db3

14 files changed

+751
-317
lines changed

config-sample.py

+15-2
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,7 @@ def generate(switch, model_id):
118118
# Template function definition
119119
#
120120
cfg = tempita.Template(file(model.template).read())
121-
return \
122-
cfg.substitute(
121+
cfg_subbed = cfg.substitute(
123122
hostname=switch,
124123
model=model,
125124
mgmt_ip=mgmt['ip'],
@@ -140,6 +139,20 @@ def generate(switch, model_id):
140139
# snmp_auth=config.snmp_auth,
141140
# snmp_priv=config.snmp_priv
142141
)
142+
# We make the Juniper config a script, to be able to add the crontab.
143+
if model_id[:7] == "Juniper":
144+
cfg_subbed = '''#!/bin/sh
145+
echo "" > /tmp/dhtech.config
146+
fullconfig=$(cat << "ENDOFCONFIG"
147+
''' + cfg_subbed + '''
148+
ENDOFCONFIG
149+
)
150+
echo "$fullconfig" >> /tmp/dhtech.config
151+
echo '@reboot sleep 30; cli -c "configure private;set chassis auto-image-upgrade;commit"' > /tmp/dhtech.cron
152+
crontab /tmp/dhtech.cron
153+
cli -c "configure private;delete;load merge /tmp/dhtech.config;commit"
154+
'''
155+
return cfg_subbed
143156

144157
def parse_metadata(switch):
145158
sql = '''SELECT n_mgmt.ipv4_txt, h.ipv4_addr_txt, n_mgmt.ipv4_gateway_txt,

dhcpd.conf

+5-1
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,16 @@ on commit {
3434
option space JUNIPER;
3535
option JUNIPER.image-file-name code 0 = text;
3636
option JUNIPER.config-file-name code 1 = text;
37+
option JUNIPER.transfer-mode code 3 = text;
3738
option JUNIPER-encapsulation code 43 = encapsulate JUNIPER;
3839
option option-150 code 150 = ip-address;
3940

4041
option OPTION-150 192.168.40.10;
4142
option JUNIPER.config-file-name "juniper-confg";
42-
option JUNIPER.image-file-name "juniper.tgz";
43+
# Enable this to auto upgrade switches. Read this first though:
44+
# https://www.reddit.com/r/Juniper/comments/ctgoyh/ex3400_ztp_storage_issues/
45+
#option JUNIPER.image-file-name "juniper.tgz";
46+
option JUNIPER.transfer-mode "http";
4347

4448
class "cisco-switch" {
4549
match if (substring (option dhcp-client-identifier, 1, 5) = "cisco") or

swhttpd.py

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/env python
2+
import sys, logging
3+
import redis
4+
import syslog
5+
import socket
6+
import re
7+
import tempfile
8+
import SimpleHTTPServer
9+
import SocketServer
10+
import time
11+
12+
import config
13+
14+
def log(*args):
15+
print time.strftime("%Y-%m-%d %H:%M:%S") + ':', ' '.join(args)
16+
syslog.syslog(syslog.LOG_INFO, ' '.join(args))
17+
18+
def error(*args):
19+
print time.strftime("%Y-%m-%d %H:%M:%S") + ': ERROR:', ' '.join(args)
20+
syslog.syslog(syslog.LOG_ERR, ' '.join(args))
21+
22+
class swbootHttpHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
23+
def do_GET(self):
24+
db = redis.Redis()
25+
switch = db.get(self.client_address[0])
26+
model = db.get('client-{}'.format(self.client_address[0]))
27+
if switch == None or model == None:
28+
log("Switch not found:", self.client_address[0])
29+
self.send_error(404, "File not found")
30+
return None
31+
if self.path == "/juniper-confg":
32+
log("Generating Juniper config for",
33+
self.client_address[0], "name =", switch)
34+
f = tempfile.TemporaryFile()
35+
f.write(config.generate(switch, model))
36+
content_length = f.tell()
37+
f.seek(0)
38+
39+
self.send_response(200)
40+
self.send_header("Content-type", "application/octet-stream")
41+
self.send_header("Content-Length", content_length)
42+
self.end_headers()
43+
self.copyfile(f, self.wfile)
44+
log("Config sent to", self.client_address[0], "name =", switch)
45+
46+
f.close()
47+
return
48+
elif self.path == "/juniper.tgz":
49+
log("Sending JunOS file", config.models[model]['image'], "to",
50+
self.client_address[0], "name =", switch)
51+
if (model in config.models) and ('image' in config.models[model]):
52+
self.path = config.models[model]['image']
53+
f = self.send_head()
54+
if f:
55+
self.copyfile(f, self.wfile)
56+
log("Sent JunOS to", self.client_address[0], "name =", switch)
57+
f.close()
58+
else:
59+
log("Unknown file:", self.path)
60+
self.send_error(404, "File not found")
61+
62+
# We write our own logs.
63+
def log_request(self, code='-', size='-'):
64+
pass
65+
def log_error(self, format, *args):
66+
pass
67+
68+
class swbootTCPServer(SocketServer.ForkingTCPServer):
69+
def server_bind(self):
70+
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
71+
self.socket.bind(self.server_address)
72+
73+
log("swhttpd started")
74+
75+
try:
76+
httpd = swbootTCPServer(("0.0.0.0", 80), swbootHttpHandler)
77+
httpd.serve_forever()
78+
except socket.error, err:
79+
sys.stderr.write("Socket error: %s\n" % str(err))
80+
sys.exit(1)
81+
except KeyboardInterrupt:
82+
sys.stderr.write("\n")
83+
except:
84+
sys.stderr.write('Something went wrong: %2' % sys.exc_info()[0])
85+
86+

switchconfig/config-example.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@ models:
3333
- name: "Juniper-ex3400-24t"
3434
path: "/scripts/swboot/switchconfig/ex3400.cfg"
3535
ports: 24
36-
image: "/scripts/swboot/junos/junos-arm-32-19.3R1.8.tgz"
36+
image: "junos/junos-arm-32-19.3R1.8.tgz"
3737
- name: "Juniper-ex3400-48t"
3838
path: "/scripts/swboot/switchconfig/ex3400.cfg"
3939
ports: 48
40-
image: "/scripts/swboot/junos/junos-arm-32-19.3R1.8.tgz"
40+
image: "junos/junos-arm-32-19.3R1.8.tgz"
4141

4242
static_files:
4343
- c2950.bin: "/scripts/swboot/ios/c2950-i6k2l2q4-mz.121-22.EA14.bin"

swtftpd.py

+20-20
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,18 @@
88
import traceback
99
import re
1010
import os
11+
import time
1112

1213
import config
1314

1415
db = redis.Redis()
1516

1617
def log(*args):
17-
print ' '.join(args)
18+
print time.strftime("%Y-%m-%d %H:%M:%S") + ':', ' '.join(args)
1819
syslog.syslog(syslog.LOG_INFO, ' '.join(args))
1920

2021
def error(*args):
21-
print 'ERROR:', ' '.join(args)
22+
print time.strftime("%Y-%m-%d %H:%M:%S") + ': ERROR:', ' '.join(args)
2223
syslog.syslog(syslog.LOG_ERR, ' '.join(args))
2324

2425
def sw_reload(ip):
@@ -94,17 +95,16 @@ def resolve_option82(relay, option82):
9495
'.1.3.6.1.4.1.9.2.2.1.1.28.%d' % int(iface))
9596
return snmpv3_command(var, relay, netsnmp.snmpget)[0][6:]
9697

97-
def file_callback(context):
98-
if context.file_to_transfer in config.static_files:
99-
return file(config.static_files[context.file_to_transfer])
98+
def file_callback(file_to_transfer, ip, rport):
99+
if file_to_transfer in config.static_files:
100+
return file(config.static_files[file_to_transfer])
100101

101102
global db
102-
option82 = db.get(context.host)
103+
option82 = db.get(ip)
103104
if option82 is None:
104-
error('No record of switch', context.host, 'in Redis, ignoring ..')
105+
error('No record of switch', ip, 'in Redis, ignoring ..')
105106
return None
106107

107-
ip = context.host
108108
# If we do not have any franken switches, do not execute this horrible code path
109109
if not config.franken_net_switches:
110110
switch = option82
@@ -132,17 +132,17 @@ def file_callback(context):
132132
print 'Switch is "%s"' % switch
133133
db.set('switchname-%s' % ip, switch)
134134

135-
if (context.file_to_transfer == "network-confg" or
136-
context.file_to_transfer == "Switch-confg"):
135+
if (file_to_transfer == "network-confg" or
136+
file_to_transfer == "Switch-confg"):
137137
f = tempfile.TemporaryFile()
138-
log("Generating base config", context.file_to_transfer,
139-
"for", context.host,"config =", switch)
138+
log("Generating base config", file_to_transfer,
139+
"for", ip,"config =", switch)
140140
base(f, switch)
141141
f.seek(0)
142142
return f
143143

144-
if context.file_to_transfer == "juniper.tgz":
145-
model = db.get('client-{}'.format(context.host))
144+
if file_to_transfer == "juniper.tgz":
145+
model = db.get('client-{}'.format(ip))
146146
if (model in config.models) and ('image' in config.models[model]):
147147
return file(config.models[model]['image'])
148148

@@ -152,27 +152,27 @@ def file_callback(context):
152152
return None
153153

154154
f = tempfile.TemporaryFile()
155-
if context.file_to_transfer.lower().endswith("-confg"):
155+
if file_to_transfer.lower().endswith("-confg"):
156156
log("Generating config for", ip,"config =", switch)
157157
if generate(f, ip, switch) == None:
158158
return None
159159
else:
160160
error("Switch", ip, "config =", switch, "tried to get file",
161-
context.file_to_transfer)
161+
file_to_transfer)
162162
f.close()
163163
return None
164164

165165
f.seek(0)
166166
return f
167167

168168
log("swtftpd started")
169-
tftpy.setLogLevel(logging.WARNING)
170-
server = tftpy.TftpServer(file_callback)
169+
server = tftpy.TftpServer('/scripts/swboot/ios', file_callback)
170+
tftplog = logging.getLogger('tftpy.TftpClient')
171+
tftplog.setLevel(logging.WARN)
171172
try:
172173
server.listen("192.168.40.10", 69)
173174
except tftpy.TftpException, err:
174175
sys.stderr.write("%s\n" % str(err))
175176
sys.exit(1)
176177
except KeyboardInterrupt:
177-
pass
178-
178+
sys.stderr.write("\n")

tftpy/README

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
Copyright, Michael P. Soulier, 2010.
22

3+
About Release 0.8.0:
4+
====================
5+
This version introduces Python 3.X support.
6+
And there was much rejoicing.
7+
8+
About Release 0.7.0:
9+
====================
10+
Various bugfixes and refactoring for improved logging.
11+
Now requiring python 2.7+ and tightening syntax in
12+
preparation for supporting python 3.
13+
14+
About Release 0.6.2:
15+
====================
16+
Maintenance release to fix a couple of reported issues.
17+
18+
About Release 0.6.1:
19+
====================
20+
Maintenance release to fix several reported problems, including a rollover
21+
at 2^16 blocks, and some contributed work on dynamic file objects.
22+
323
About Release 0.6.0:
424
====================
525
Maintenance update to fix several reported issues, including proper
@@ -85,7 +105,7 @@ easy inclusion in a UI for populating progress indicators. It supports RFCs
85105

86106
Dependencies:
87107
-------------
88-
Python 2.3+, hopefully. Let me know if it fails to work.
108+
Python 2.7+, hopefully. Let me know if it fails to work.
89109

90110
Trifles:
91111
--------

tftpy/TftpClient.py

+20-13
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,36 @@
1+
# vim: ts=4 sw=4 et ai:
2+
# -*- coding: utf8 -*-
13
"""This module implements the TFTP Client functionality. Instantiate an
24
instance of the client, and then use its upload or download method. Logging is
35
performed via a standard logging object set in TftpShared."""
46

7+
58
import types
6-
from TftpShared import *
7-
from TftpPacketTypes import *
8-
from TftpContexts import TftpContextClientDownload, TftpContextClientUpload
9+
import logging
10+
from .TftpShared import *
11+
from .TftpPacketTypes import *
12+
from .TftpContexts import TftpContextClientDownload, TftpContextClientUpload
13+
14+
log = logging.getLogger('tftpy.TftpClient')
915

1016
class TftpClient(TftpSession):
1117
"""This class is an implementation of a tftp client. Once instantiated, a
1218
download can be initiated via the download() method, or an upload via the
1319
upload() method."""
1420

15-
def __init__(self, host, port, options={}):
21+
def __init__(self, host, port=69, options={}, localip = ""):
1622
TftpSession.__init__(self)
1723
self.context = None
1824
self.host = host
1925
self.iport = port
2026
self.filename = None
2127
self.options = options
22-
if self.options.has_key('blksize'):
28+
self.localip = localip
29+
if 'blksize' in self.options:
2330
size = self.options['blksize']
24-
tftpassert(types.IntType == type(size), "blksize must be an int")
31+
tftpassert(int == type(size), "blksize must be an int")
2532
if size < MIN_BLKSIZE or size > MAX_BLKSIZE:
26-
raise TftpException, "Invalid blksize: %d" % size
33+
raise TftpException("Invalid blksize: %d" % size)
2734

2835
def download(self, filename, output, packethook=None, timeout=SOCK_TIMEOUT):
2936
"""This method initiates a tftp download from the configured remote
@@ -38,17 +45,16 @@ def download(self, filename, output, packethook=None, timeout=SOCK_TIMEOUT):
3845
Note: If output is a hyphen, stdout is used."""
3946
# We're downloading.
4047
log.debug("Creating download context with the following params:")
41-
log.debug("host = %s, port = %s, filename = %s, output = %s"
42-
% (self.host, self.iport, filename, output))
43-
log.debug("options = %s, packethook = %s, timeout = %s"
44-
% (self.options, packethook, timeout))
48+
log.debug("host = %s, port = %s, filename = %s" % (self.host, self.iport, filename))
49+
log.debug("options = %s, packethook = %s, timeout = %s" % (self.options, packethook, timeout))
4550
self.context = TftpContextClientDownload(self.host,
4651
self.iport,
4752
filename,
4853
output,
4954
self.options,
5055
packethook,
51-
timeout)
56+
timeout,
57+
localip = self.localip)
5258
self.context.start()
5359
# Download happens here
5460
self.context.end()
@@ -82,7 +88,8 @@ def upload(self, filename, input, packethook=None, timeout=SOCK_TIMEOUT):
8288
input,
8389
self.options,
8490
packethook,
85-
timeout)
91+
timeout,
92+
localip = self.localip)
8693
self.context.start()
8794
# Upload happens here
8895
self.context.end()

0 commit comments

Comments
 (0)