Compare commits
3 commits
main
...
kunsi-juno
Author | SHA1 | Date | |
---|---|---|---|
f1a775b5c9 | |||
6ae90733c3 | |||
4f75c95c20 |
9 changed files with 1882 additions and 0 deletions
|
@ -22,3 +22,6 @@ indent_size = unset
|
|||
[*.vault]
|
||||
end_of_line = unset
|
||||
insert_final_newline = unset
|
||||
|
||||
[*.json]
|
||||
insert_final_newline = unset
|
||||
|
|
141
configs/junos-template.conf
Normal file
141
configs/junos-template.conf
Normal file
|
@ -0,0 +1,141 @@
|
|||
version ${'.'.join(node.metadata.get('junos/version'))};
|
||||
system {
|
||||
host-name ${node.name.split('.')[-1]};
|
||||
time-zone GMT;
|
||||
root-authentication {
|
||||
encrypted-password "$5$1hGrR8Kk$lx3CIdxqvesBrZUtDftROEoyXQuMENEu62JVtHw6WGD"; ## SECRET-DATA
|
||||
}
|
||||
name-server {
|
||||
% for srv in repo.libs.defaults.nameservers_ipv4:
|
||||
${srv};
|
||||
% endfor
|
||||
}
|
||||
login {
|
||||
% for uid, (uname, uconfig) in enumerate(sorted(users.items())):
|
||||
user ${uname} {
|
||||
full-name ${uname};
|
||||
uid ${1000+uid};
|
||||
class super-user;
|
||||
authentication {
|
||||
% for pubkey in sorted(uconfig['ssh_pubkey']):
|
||||
${pubkey.split(' ', 1)[0]} "${pubkey}";
|
||||
% endfor
|
||||
}
|
||||
}
|
||||
% endfor
|
||||
}
|
||||
services {
|
||||
ssh {
|
||||
protocol-version v2;
|
||||
}
|
||||
netconf {
|
||||
ssh;
|
||||
}
|
||||
# web-management {
|
||||
# http;
|
||||
# }
|
||||
}
|
||||
syslog {
|
||||
user * {
|
||||
any emergency;
|
||||
}
|
||||
file messages {
|
||||
any notice;
|
||||
authorization info;
|
||||
}
|
||||
file interactive-commands {
|
||||
interactive-commands any;
|
||||
}
|
||||
}
|
||||
ntp {
|
||||
% for srv in sorted(ntp_servers):
|
||||
server ${srv};
|
||||
% endfor;
|
||||
}
|
||||
}
|
||||
interfaces {
|
||||
% for iface, config in sorted(interfaces.items()):
|
||||
${iface} {
|
||||
unit 0 {
|
||||
% if not config['enabled']:
|
||||
disable;
|
||||
% endif
|
||||
% if config['mode'] == 'trunk':
|
||||
family ethernet-switching {
|
||||
port-mode trunk;
|
||||
vlan {
|
||||
members [ ${' '.join(sorted(config['tagged_vlans']))} ];
|
||||
}
|
||||
% if config['untagged_vlan']:
|
||||
native-vlan-id ${config['untagged_vlan']};
|
||||
% endif
|
||||
}
|
||||
% else:
|
||||
family ethernet-switching;
|
||||
% endif
|
||||
}
|
||||
}
|
||||
% endfor
|
||||
vlan {
|
||||
% for idx, (vlan, vconfig) in enumerate(sorted(vlans.items())):
|
||||
% if vconfig['ip_address']:
|
||||
unit ${idx} {
|
||||
family inet {
|
||||
address ${vconfig['ip_address']};
|
||||
}
|
||||
}
|
||||
% endif
|
||||
% endfor
|
||||
}
|
||||
}
|
||||
snmp {
|
||||
contact "${repo.libs.defaults.hostmaster_email}";
|
||||
community public {
|
||||
authorization read-only;
|
||||
}
|
||||
}
|
||||
routing-options {
|
||||
static {
|
||||
route 0.0.0.0/0 next-hop ${gateway};
|
||||
}
|
||||
}
|
||||
protocols {
|
||||
igmp-snooping {
|
||||
vlan all;
|
||||
}
|
||||
rstp;
|
||||
lldp {
|
||||
interface all;
|
||||
}
|
||||
lldp-med {
|
||||
interface all;
|
||||
}
|
||||
}
|
||||
ethernet-switching-options {
|
||||
voip;
|
||||
storm-control {
|
||||
interface all;
|
||||
}
|
||||
}
|
||||
vlans {
|
||||
% for idx, (vlan, vconfig) in enumerate(sorted(vlans.items())):
|
||||
${vlan} {
|
||||
% if vconfig['id']:
|
||||
vlan-id ${vconfig['id']};
|
||||
% endif
|
||||
interface {
|
||||
% for iface, iconfig in sorted(interfaces.items()):
|
||||
% if iconfig['untagged_vlan'] == vlan:
|
||||
${iface}.0;
|
||||
% endif
|
||||
% endfor
|
||||
}
|
||||
% if vconfig['ip_address']:
|
||||
l3-interface vlan.${idx};
|
||||
% endif
|
||||
}
|
||||
% endfor
|
||||
}
|
||||
poe {
|
||||
interface all;
|
||||
}
|
|
@ -88,3 +88,10 @@ groups['debian-bullseye'] = {
|
|||
groups['debian-sid'] = {
|
||||
'os_version': (99,)
|
||||
}
|
||||
|
||||
groups['junos'] = {
|
||||
'dummy': True,
|
||||
'cmd_wrapper_outer': '{}',
|
||||
'cmd_wrapper_inner': '{}',
|
||||
'os': 'freebsd',
|
||||
}
|
||||
|
|
149
libs/juniper.py
Normal file
149
libs/juniper.py
Normal file
|
@ -0,0 +1,149 @@
|
|||
import random
|
||||
|
||||
# copied from https://github.com/peering-manager/peering-manager/blob/main/devices/crypto/juniper.py
|
||||
|
||||
|
||||
# This code is the result of the attempt at converting a Perl module, the expected
|
||||
# result might not actually be what we really want it to be ¯\_(ツ)_/¯
|
||||
#
|
||||
# https://metacpan.org/pod/Crypt::Juniper
|
||||
|
||||
|
||||
MAGIC = "$9$"
|
||||
|
||||
FAMILY = [
|
||||
"QzF3n6/9CAtpu0O",
|
||||
"B1IREhcSyrleKvMW8LXx",
|
||||
"7N-dVbwsY2g4oaJZGUDj",
|
||||
"iHkq.mPf5T",
|
||||
]
|
||||
EXTRA = {}
|
||||
for counter, value in enumerate(FAMILY):
|
||||
for character in value:
|
||||
EXTRA[character] = 3 - counter
|
||||
|
||||
NUM_ALPHA = [x for x in "".join(FAMILY)]
|
||||
ALPHA_NUM = {NUM_ALPHA[x]: x for x in range(0, len(NUM_ALPHA))}
|
||||
|
||||
ENCODING = [
|
||||
[1, 4, 32],
|
||||
[1, 16, 32],
|
||||
[1, 8, 32],
|
||||
[1, 64],
|
||||
[1, 32],
|
||||
[1, 4, 16, 128],
|
||||
[1, 32, 64],
|
||||
]
|
||||
|
||||
|
||||
def __nibble(cref, length):
|
||||
nib = cref[0:length]
|
||||
rest = cref[length:]
|
||||
|
||||
if len(nib) != length:
|
||||
raise Exception(f"Ran out of characters: hit '{nib}', expecting {length} chars")
|
||||
|
||||
return nib, rest
|
||||
|
||||
|
||||
def __gap(c1, c2):
|
||||
return (ALPHA_NUM[str(c2)] - ALPHA_NUM[str(c1)]) % (len(NUM_ALPHA)) - 1
|
||||
|
||||
|
||||
def __gap_decode(gaps, dec):
|
||||
num = 0
|
||||
|
||||
if len(gaps) != len(dec):
|
||||
raise Exception("Nibble and decode size not the same.")
|
||||
|
||||
for x in range(0, len(gaps)):
|
||||
num += gaps[x] * dec[x]
|
||||
|
||||
return chr(num % 256)
|
||||
|
||||
|
||||
def __reverse(current):
|
||||
reversed = list(current)
|
||||
reversed.reverse()
|
||||
return reversed
|
||||
|
||||
|
||||
def __gap_encode(pc, prev, encode):
|
||||
__ord = ord(pc)
|
||||
|
||||
crypt = ""
|
||||
gaps = []
|
||||
for mod in __reverse(encode):
|
||||
gaps.insert(0, int(__ord / mod))
|
||||
__ord %= mod
|
||||
|
||||
for gap in gaps:
|
||||
gap += ALPHA_NUM[prev] + 1
|
||||
prev = NUM_ALPHA[gap % len(NUM_ALPHA)]
|
||||
crypt += prev
|
||||
|
||||
return crypt
|
||||
|
||||
|
||||
def __randc(counter=0):
|
||||
return_value = ""
|
||||
for _ in range(counter):
|
||||
return_value += NUM_ALPHA[random.randrange(len(NUM_ALPHA))]
|
||||
return return_value
|
||||
|
||||
|
||||
def is_encrypted(value):
|
||||
return value.startswith(MAGIC)
|
||||
|
||||
|
||||
def decrypt(value):
|
||||
if not value:
|
||||
return ""
|
||||
|
||||
if not is_encrypted(value):
|
||||
return value
|
||||
|
||||
chars = value.split("$9$", 1)[1]
|
||||
first, chars = __nibble(chars, 1)
|
||||
toss, chars = __nibble(chars, EXTRA[first])
|
||||
previous = first
|
||||
decrypted = ""
|
||||
|
||||
while chars:
|
||||
decode = ENCODING[len(decrypted) % len(ENCODING)]
|
||||
nibble, chars = __nibble(chars, len(decode))
|
||||
gaps = []
|
||||
for i in nibble:
|
||||
g = __gap(previous, i)
|
||||
previous = i
|
||||
gaps += [g]
|
||||
decrypted += __gap_decode(gaps, decode)
|
||||
|
||||
return decrypted
|
||||
|
||||
|
||||
def encrypt(value, salt=None):
|
||||
if not value:
|
||||
return ""
|
||||
|
||||
if not isinstance(value, str):
|
||||
value = str(value)
|
||||
|
||||
if is_encrypted(value):
|
||||
return value
|
||||
|
||||
if not salt:
|
||||
salt = __randc(1)
|
||||
rand = __randc(EXTRA[salt])
|
||||
|
||||
position = 0
|
||||
previous = salt
|
||||
crypted = MAGIC + salt + rand
|
||||
|
||||
for x in value:
|
||||
encode = ENCODING[position % len(ENCODING)]
|
||||
crypted += __gap_encode(x, previous, encode)
|
||||
previous = crypted[-1]
|
||||
position += 1
|
||||
|
||||
return crypted
|
1322
netbox_dump.json
Normal file
1322
netbox_dump.json
Normal file
File diff suppressed because it is too large
Load diff
5
nodes/home/home.sw02.toml
Normal file
5
nodes/home/home.sw02.toml
Normal file
|
@ -0,0 +1,5 @@
|
|||
hostname = "172.19.138.4"
|
||||
groups = ["junos"]
|
||||
|
||||
[metadata.junos]
|
||||
version = ["15", "1R5", "5"]
|
|
@ -1,3 +1,4 @@
|
|||
bundlewrap~=4.16.0
|
||||
PyNaCl
|
||||
bundlewrap-pass
|
||||
pynetbox==7.0.0
|
||||
|
|
137
scripts/junos-update-config
Executable file
137
scripts/junos-update-config
Executable file
|
@ -0,0 +1,137 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from json import load
|
||||
from os import environ
|
||||
from os.path import join
|
||||
from sys import argv, exit
|
||||
from tempfile import gettempdir
|
||||
|
||||
from mako.template import Template
|
||||
|
||||
from bundlewrap.repo import Repository
|
||||
from bundlewrap.utils.text import bold
|
||||
from bundlewrap.utils.ui import io
|
||||
|
||||
NTP_SERVERS = {
|
||||
# pool.ntp.org
|
||||
'148.251.54.81',
|
||||
'162.159.200.123',
|
||||
'213.209.109.44',
|
||||
'54.36.110.36',
|
||||
}
|
||||
|
||||
try:
|
||||
node_name = argv[1]
|
||||
except Exception:
|
||||
print(f'Usage: {argv[0]} <node name>')
|
||||
exit(1)
|
||||
|
||||
path = environ.get('BW_REPO_PATH', '.')
|
||||
repo = Repository(path)
|
||||
node = repo.get_node(node_name)
|
||||
|
||||
try:
|
||||
io.activate()
|
||||
|
||||
interfaces = {}
|
||||
users = {}
|
||||
vlans = {
|
||||
'default': {
|
||||
'id': None,
|
||||
'ip_address': '169.254.254.254/24',
|
||||
},
|
||||
}
|
||||
|
||||
tmpfile = join(gettempdir(), f'{node.name}.conf')
|
||||
|
||||
gw_split = node.hostname.split('.')
|
||||
gw_split[3] = '1'
|
||||
gateway = '.'.join(gw_split)
|
||||
|
||||
with io.job('reading netbox_dump.json'):
|
||||
with open(join(repo.path, 'netbox_dump.json'), 'r') as f:
|
||||
json = load(f)[node.metadata.get('location')]
|
||||
|
||||
for vlan, vid in json['vlans'].items():
|
||||
vlans[vlan] = {
|
||||
'id': vid,
|
||||
'ip_address': None,
|
||||
}
|
||||
|
||||
for iface, iconfig in json['devices'][node.name].items():
|
||||
if iface in vlans:
|
||||
# If the interface name is the same as a vlan name, this
|
||||
# means the ip assigned to this interface should get
|
||||
# assigned to that vlan.
|
||||
vlans[iface]['ip_address'] = iconfig['ip_addresses'][0]
|
||||
else:
|
||||
interfaces[iface] = {
|
||||
'enabled': bool(
|
||||
iconfig['enabled']
|
||||
and iconfig['mode']
|
||||
and (
|
||||
iconfig['vlans']['tagged']
|
||||
or iconfig['vlans']['untagged']
|
||||
)
|
||||
),
|
||||
'description': iconfig['description'],
|
||||
'untagged_vlan': iconfig['vlans']['untagged'],
|
||||
}
|
||||
|
||||
if iconfig['mode'] and iconfig['mode'].startswith('tagged'):
|
||||
interfaces[iface]['mode'] = 'trunk'
|
||||
else:
|
||||
interfaces[iface]['mode'] = 'access'
|
||||
|
||||
tagged_vlans = set()
|
||||
for vlan in iconfig['vlans']['tagged']:
|
||||
tagged_vlans.add(str(vlans[vlan]['id']))
|
||||
interfaces[iface]['tagged_vlans'] = tagged_vlans
|
||||
|
||||
with io.job('reading users.json'):
|
||||
with open(join(repo.path, 'users.json'), 'r') as f:
|
||||
json = load(f)
|
||||
|
||||
users = {}
|
||||
for uname, config in json.items():
|
||||
if config.get('is_admin', False):
|
||||
users[uname] = {
|
||||
'password': repo.vault.password_for(f'{node.name} {uname} login'),
|
||||
'ssh_pubkey': set(config['ssh_pubkey']),
|
||||
}
|
||||
|
||||
|
||||
with io.job(f'{bold(node.name)} rendering config template to {tmpfile}'):
|
||||
with open(join(repo.path, 'configs', 'junos-template.conf')) as f:
|
||||
template = Template(
|
||||
f.read().encode('utf-8'),
|
||||
input_encoding='utf-8',
|
||||
output_encoding='utf-8',
|
||||
)
|
||||
content = template.render(
|
||||
gateway=gateway,
|
||||
interfaces=interfaces,
|
||||
node=node,
|
||||
ntp_servers=NTP_SERVERS,
|
||||
repo=repo,
|
||||
users=users,
|
||||
vlans=vlans,
|
||||
)
|
||||
with open(tmpfile, 'w+') as f:
|
||||
f.write(content.decode('utf-8'))
|
||||
|
||||
with io.job(f'{bold(node.name)} updating configuration on device'):
|
||||
node.upload(tmpfile, '/tmp/bundlewrap.conf')
|
||||
|
||||
result = node.run(
|
||||
'configure exclusive ; load override /tmp/bundlewrap.conf ; commit',
|
||||
log_output=True,
|
||||
)
|
||||
|
||||
if 'commit complete' in result.stdout.decode():
|
||||
node.run(
|
||||
'request system configuration rescue save',
|
||||
log_output=True,
|
||||
)
|
||||
finally:
|
||||
io.deactivate()
|
117
scripts/netbox-dump
Executable file
117
scripts/netbox-dump
Executable file
|
@ -0,0 +1,117 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from json import dump
|
||||
from os import environ
|
||||
from os.path import join
|
||||
from sys import exit
|
||||
|
||||
from bundlewrap.utils.ui import QUIT_EVENT, io
|
||||
from bundlewrap.utils.text import bold, yellow, validate_name
|
||||
from pynetbox import api as netbox_api
|
||||
|
||||
|
||||
BW_REPO_PATH = environ.get('BW_REPO_PATH', '.')
|
||||
netbox = netbox_api(
|
||||
environ.get('NETBOX_HOST', 'https://netbox.franzi.business'),
|
||||
token=environ.get('NETBOX_TOKEN', None),
|
||||
)
|
||||
|
||||
result = {
|
||||
# 'my_site_name': {
|
||||
# 'vlans': {
|
||||
# 'my_vlan_name': 10,
|
||||
# 'other_vlan_name': 11,
|
||||
# 'yet_another_vlan_name': 12,
|
||||
# },
|
||||
# 'devices': {
|
||||
# 'my_switch': {
|
||||
# 'port1': {
|
||||
# 'description': 'foo',
|
||||
# 'type': '1000base-t', # or 'lag'
|
||||
# 'mode': None, # or 'access', 'tagged', 'tagged-all'
|
||||
# 'lag': 'none', # or 'LAG1'
|
||||
# 'vlan': {
|
||||
# 'untagged': 'my_vlan_name',
|
||||
# 'tagged': [
|
||||
# 'other_vlan_name',
|
||||
# 'yet_another_vlan_name',
|
||||
# ],
|
||||
# },
|
||||
# },
|
||||
# },
|
||||
# },
|
||||
# },
|
||||
}
|
||||
|
||||
errors = False
|
||||
try:
|
||||
io.activate()
|
||||
|
||||
for site in netbox.dcim.sites.all():
|
||||
site_name = site.name.lower()
|
||||
|
||||
result[site_name] = {
|
||||
'vlans': {},
|
||||
'devices': {},
|
||||
}
|
||||
|
||||
with io.job(f'{bold(site_name)} getting vlans'):
|
||||
for vlan in netbox.ipam.vlans.filter(site_id=site.id):
|
||||
if vlan.name in result[site_name]['vlans'].keys() and result[site_name]['vlans'][vlan.name] != vlan.id:
|
||||
raise Exception(f"vlan {result[site_name]['vlans'][vlan.name]} and {vlan.id} both have the name {vlan.name}")
|
||||
|
||||
result[site_name]['vlans'][vlan.name] = vlan.vid
|
||||
|
||||
for interface in netbox.dcim.interfaces.filter(site_id=site.id):
|
||||
if QUIT_EVENT.is_set():
|
||||
exit(0)
|
||||
|
||||
with io.job(f'{bold(site_name)} {bold(interface.device.name)} interface {yellow(interface.name)}'):
|
||||
if not interface.device.name:
|
||||
# Unnamed device. Probably not managed by bw.
|
||||
continue
|
||||
elif not validate_name(interface.device.name):
|
||||
# bundlewrap does not consider this device name to be a valid
|
||||
# node name. Ignore it, we don't manage it
|
||||
continue
|
||||
|
||||
has_valid_description = False
|
||||
if interface.description:
|
||||
description = interface.description
|
||||
has_valid_description = True
|
||||
elif interface.connected_endpoints:
|
||||
description = f'{sorted(interface.connected_endpoints)[0].device.display} ({sorted(interface.connected_endpoints)[0].display})'
|
||||
has_valid_description = True
|
||||
elif interface.link_peers:
|
||||
description = f'{sorted(interface.link_peers)[0].device.display} ({sorted(interface.link_peers)[0].display})'
|
||||
else:
|
||||
description = ''
|
||||
|
||||
if not description.isascii():
|
||||
errors = True
|
||||
io.stderr(f'{bold(interface.device.name)} {bold(interface.name)} description "{description}" contains non-ascii characters, this isn\'t supported')
|
||||
|
||||
result[site_name]['devices'].setdefault(interface.device.name, {})[interface.name] = {
|
||||
'description': description,
|
||||
'enabled': interface.enabled,
|
||||
'ip_addresses': sorted(set() if interface.count_ipaddresses == 0 else {
|
||||
ip.address for ip in
|
||||
netbox.ipam.ip_addresses.filter(interface_id=interface.id)
|
||||
}),
|
||||
'mode': interface.mode.value if interface.mode else None,
|
||||
'type': interface.type.value,
|
||||
'lag': interface.lag.name if interface.lag else None,
|
||||
'vlans': {
|
||||
'untagged': interface.untagged_vlan.name if interface.untagged_vlan else None,
|
||||
'tagged': sorted(vlan.name for vlan in interface.tagged_vlans),
|
||||
},
|
||||
}
|
||||
|
||||
if errors:
|
||||
exit(1)
|
||||
|
||||
with io.job('dumping result to netbox_dump.json'):
|
||||
with open(join(BW_REPO_PATH, 'netbox_dump.json'), 'w') as f:
|
||||
dump(result, f, indent=4, sort_keys=True)
|
||||
finally:
|
||||
io.deactivate()
|
Loading…
Reference in a new issue