-
-
Notifications
You must be signed in to change notification settings - Fork 190
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[connection] Init new connection module
Requires django-netjsonconfig 0.8.1 Allows to - connect to devices using different protocols - SSH protocol supported by default - centrally launch update config operations when config/templates are changed Signed-off-by: Federico Capoano <[email protected]>
- Loading branch information
1 parent
cc7e9ff
commit 277c489
Showing
18 changed files
with
675 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
default_app_config = 'openwisp_controller.connection.apps.ConnectionConfig' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
from django.contrib import admin | ||
|
||
from openwisp_utils.admin import MultitenantOrgFilter, TimeReadonlyAdminMixin | ||
|
||
from ..admin import MultitenantAdminMixin | ||
from ..config.admin import DeviceAdmin | ||
from .models import Credentials, DeviceConnection, DeviceIp | ||
|
||
|
||
@admin.register(Credentials) | ||
class CredentialsAdmin(MultitenantAdminMixin, TimeReadonlyAdminMixin, admin.ModelAdmin): | ||
list_display = ('name', 'organization', 'connector', 'created', 'modified') | ||
list_filter = [('organization', MultitenantOrgFilter), | ||
'connector'] | ||
list_select_related = ('organization',) | ||
|
||
|
||
class DeviceIpInline(admin.TabularInline): | ||
model = DeviceIp | ||
exclude = ('created', 'modified') | ||
extra = 0 | ||
|
||
def get_queryset(self, request): | ||
qs = super(DeviceIpInline, self).get_queryset(request) | ||
return qs.order_by('priority') | ||
|
||
|
||
class DeviceConnectionInline(admin.StackedInline): | ||
model = DeviceConnection | ||
exclude = ['params', 'created', 'modified'] | ||
readonly_fields = ['is_working', 'failure_reason', 'last_attempt'] | ||
extra = 0 | ||
|
||
|
||
DeviceAdmin.inlines += [DeviceConnectionInline, DeviceIpInline] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
from django.apps import AppConfig | ||
from django.utils.translation import ugettext_lazy as _ | ||
from django_netjsonconfig.signals import config_modified | ||
|
||
|
||
class ConnectionConfig(AppConfig): | ||
name = 'openwisp_controller.connection' | ||
label = 'connection' | ||
verbose_name = _('Network Device Credentials') | ||
|
||
def ready(self): | ||
# connect to config_modified signal | ||
from .models import DeviceConnection | ||
config_modified.connect(DeviceConnection.config_modified_receiver) |
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from ..ssh import Ssh | ||
|
||
|
||
class OpenWrt(Ssh): | ||
def update_config(self): | ||
self.shell.exec_command('/etc/init.d/openwisp_config restart') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import ipaddress | ||
import logging | ||
|
||
import paramiko | ||
from django.core.exceptions import ObjectDoesNotExist | ||
from django.utils.functional import cached_property | ||
from jsonschema import validate | ||
from jsonschema.exceptions import ValidationError as SchemaError | ||
from scp import SCPClient | ||
|
||
from ..utils import get_interfaces | ||
|
||
try: | ||
from io import StringIO | ||
except ImportError: | ||
from StringIO import StringIO | ||
|
||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class Ssh(object): | ||
schema = { | ||
"$schema": "http://json-schema.org/draft-04/schema#", | ||
"type": "object", | ||
"additionalProperties": False, | ||
"required": ["username"], | ||
"properties": { | ||
"username": {"type": "string"}, | ||
"password": {"type": "string"}, | ||
"key": {"type": "string"}, | ||
"port": {"type": "integer"}, | ||
} | ||
} | ||
|
||
def __init__(self, device_connection): | ||
self.connection = device_connection | ||
self.device = device_connection.device | ||
self.shell = paramiko.SSHClient() | ||
self.shell.set_missing_host_key_policy(paramiko.AutoAddPolicy()) | ||
|
||
@classmethod | ||
def validate(cls, params): | ||
validate(params, cls.schema) | ||
cls.custom_validation(params) | ||
|
||
@classmethod | ||
def custom_validation(cls, params): | ||
if 'password' not in params and 'key' not in params: | ||
raise SchemaError('Missing password or key') | ||
|
||
@cached_property | ||
def _addresses(self): | ||
deviceip_set = list(self.device.deviceip_set.all() | ||
.only('address') | ||
.order_by('priority')) | ||
address_list = [] | ||
for deviceip in deviceip_set: | ||
address = deviceip.address | ||
ip = ipaddress.ip_address(address) | ||
if not ip.is_link_local: | ||
address_list.append(address) | ||
else: | ||
for interface in get_interfaces(): | ||
address_list.append('{0}%{1}'.format(address, interface)) | ||
try: | ||
address_list.append(self.device.config.last_ip) | ||
except ObjectDoesNotExist: | ||
pass | ||
return address_list | ||
|
||
@cached_property | ||
def _params(self): | ||
params = self.connection.get_params() | ||
if 'key' in params: | ||
key_fileobj = StringIO(params.pop('key')) | ||
params['pkey'] = paramiko.RSAKey.from_private_key(key_fileobj) | ||
return params | ||
|
||
def connect(self): | ||
success = False | ||
exception = None | ||
for address in self._addresses: | ||
try: | ||
self.shell.connect(address, **self._params) | ||
except Exception as e: | ||
exception = e | ||
else: | ||
success = True | ||
break | ||
if not success: | ||
raise exception | ||
|
||
def disconnect(self): | ||
self.shell.close() | ||
|
||
def update_config(self): | ||
raise NotImplementedError() | ||
|
||
def upload(self, fl, remote_path): | ||
scp = SCPClient(self.shell.get_transport()) | ||
scp.putfo(fl, remote_path) | ||
scp.close() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
# Generated by Django 2.0.5 on 2018-05-05 17:33 | ||
|
||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
import django.utils.timezone | ||
import jsonfield.fields | ||
import model_utils.fields | ||
import openwisp_users.mixins | ||
import uuid | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
initial = True | ||
|
||
dependencies = [ | ||
('config', '0012_auto_20180219_1501'), | ||
('openwisp_users', '0002_auto_20180219_1409'), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='Credentials', | ||
fields=[ | ||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), | ||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), | ||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), | ||
('name', models.CharField(db_index=True, max_length=64, unique=True)), | ||
('connector', models.CharField(choices=[('openwisp_controller.connection.connectors.ssh.Ssh', 'SSH')], db_index=True, max_length=128, verbose_name='connection type')), | ||
('params', jsonfield.fields.JSONField(default=dict, help_text='global connection parameters', verbose_name='parameters')), | ||
('organization', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='openwisp_users.Organization', verbose_name='organization')), | ||
], | ||
options={ | ||
'verbose_name_plural': 'Access credentials', | ||
'verbose_name': 'Access credentials', | ||
}, | ||
bases=(openwisp_users.mixins.ValidateOrgMixin, models.Model), | ||
), | ||
migrations.CreateModel( | ||
name='DeviceConnection', | ||
fields=[ | ||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), | ||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), | ||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), | ||
('update_strategy', models.CharField(blank=True, choices=[('openwisp_controller.connection.connectors.openwrt.ssh.OpenWrt', 'OpenWRT SSH')], db_index=True, help_text='leave blank to determine automatically', max_length=128, verbose_name='update strategy')), | ||
('enabled', models.BooleanField(db_index=True, default=True)), | ||
('params', jsonfield.fields.JSONField(blank=True, default=dict, help_text='local connection parameters (will override the global parameters if specified)', verbose_name='parameters')), | ||
('is_working', models.NullBooleanField(default=None)), | ||
('last_attempt', models.DateTimeField(blank=True, null=True)), | ||
('failure_reason', models.CharField(blank=True, max_length=128, verbose_name='reason of failure')), | ||
('credentials', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='connection.Credentials')), | ||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='config.Device')), | ||
], | ||
options={ | ||
'verbose_name_plural': 'Device connections', | ||
'verbose_name': 'Device connection', | ||
}, | ||
), | ||
migrations.CreateModel( | ||
name='DeviceIp', | ||
fields=[ | ||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), | ||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), | ||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), | ||
('address', models.GenericIPAddressField(verbose_name='IP address')), | ||
('priority', models.PositiveSmallIntegerField()), | ||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='config.Device')), | ||
], | ||
options={ | ||
'verbose_name_plural': 'Device IP addresses', | ||
'verbose_name': 'Device IP', | ||
}, | ||
), | ||
] |
Empty file.
Oops, something went wrong.