Giter Site home page Giter Site logo

openwisp / openwisp-firmware-upgrader Goto Github PK

View Code? Open in Web Editor NEW
49.0 14.0 58.0 52.01 MB

Firmware upgrade solution for OpenWRT with possibility to add support for other embedded OSes. Provides features like automatic retry for network failures, mass upgrades, REST API and more.

Home Page: http://openwisp.org

License: Other

Python 98.11% HTML 0.97% Shell 0.15% JavaScript 0.51% CSS 0.25%
openwrt firmware-upgrader openwisp python django hacktoberfest

openwisp-firmware-upgrader's Introduction

openwisp-firmware-upgrader

https://github.com/openwisp/openwisp-firmware-upgrader/workflows/OpenWISP%20Firmware%20Upgrader%20CI%20Build/badge.svg?branch=master Dependency monitoring support chat Pypi Version Downloads code style: black

Need a quick overview? Try the OpenWISP Demo.

Firmware upgrade module of OpenWISP.

Features:

  • Stores information of each upgrade operation which can be seen from the device page
  • Automatic retries for recoverable failures (eg: firmware image upload issues because of intermittent internet connection)
  • Performs a final check to find out if the upgrade completed successfully or not
  • Prevents accidental multiple upgrades using the same firmware image
  • Single device upgrade
  • Mass upgrades
  • Possibility to divide firmware images in categories
  • REST API
  • Possibility of writing custom upgraders for other firmware OSes or for custom OpenWRT based firmwares
  • Configurable timeouts
  • Extensible

For a more complete overview of the OpenWISP modules and architecture, see the OpenWISP Architecture Overview.

Want to help OpenWISP? Find out how to help us grow here.


Table of Contents:


Installation instructions

Requirements

  • Python >= 3.8
  • openwisp-controller (and its dependencies) >= 1.0.0

Install Dependencies

Install spatialite and sqlite:

sudo apt-get install sqlite3 libsqlite3-dev openssl libssl-dev
sudo apt-get install gdal-bin libproj-dev libgeos-dev libspatialite-dev

Setup (integrate in an existing Django project)

Follow the setup instructions of openwisp-controller, then add the settings described below.

INSTALLED_APPS = [
    # django apps
    # all-auth
    'django.contrib.sites',
    'openwisp_users.accounts',
    'allauth',
    'allauth.account',
    'django_extensions',
    'private_storage',
    # openwisp2 modules
    'openwisp_controller.pki',
    'openwisp_controller.config',
    'openwisp_controller.connection',
    'openwisp_controller.geo',
    'openwisp_firmware_upgrader',
    'openwisp_users',
    'openwisp_notifications',
    'openwisp_ipam',
    # openwisp2 admin theme (must be loaded here)
    'openwisp_utils.admin_theme',
    # admin
    'django.contrib.admin',
    'django.forms',
    # other dependencies
    'sortedm2m',
    'reversion',
    'leaflet',
    'flat_json_widget',
    # rest framework
    'rest_framework',
    'rest_framework.authtoken',
    'rest_framework_gis',
    'django_filters',
    'drf_yasg',
    # channels
    'channels',
]

MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
PRIVATE_STORAGE_ROOT = os.path.join(MEDIA_ROOT, 'firmware')

The root URLconf (urls.py) should look like the following example:

from django.conf import settings
from django.contrib import admin
from django.conf.urls import include, url
from django.contrib.staticfiles.urls import staticfiles_urlpatterns

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('openwisp_controller.urls')),
    path('', redirect_view, name='index'),
    path('', include('openwisp_firmware_upgrader.urls')),
    path('api/v1/', include((get_api_urls(), 'users'), namespace='users')),
    path('api/v1/', include('openwisp_utils.api.urls')),
]

urlpatterns += staticfiles_urlpatterns()

Installing for development

Install your forked repo:

git clone git://github.com/<your_fork>/openwisp-firmware-upgrader
cd openwisp-firmware-upgrader/
python setup.py develop

Install test requirements:

pip install -r requirements-test.txt

Create database:

cd tests/
./manage.py migrate
./manage.py createsuperuser

Launch development server:

./manage.py runserver 0.0.0.0:8000

You can access the admin interface at http://127.0.0.1:8000/admin/.

Run celery and celery-beat with the following commands (separate terminal windows are needed):

# (cd tests)
celery -A openwisp2 worker -l info
celery -A openwisp2 beat -l info

Run tests with:

# run qa checks
./run-qa-checks

# standard tests
./runtests.py

# tests for the sample app
SAMPLE_APP=1 ./runtests.py --keepdb --failfast

When running the last line of the previous example, the environment variable SAMPLE_APP activates the app in /tests/openwisp2/sample_firmware_upgrader/ which is a simple django app that extends openwisp-firmware-upgrader with the sole purpose of testing its extensibility, for more information regarding this concept, read the following section.

Quickstart Guide

Requirements:

  • Devices running at least OpenWRT 12.09 Attitude Adjustment, older versions of OpenWRT have not worked at all in our tests
  • Devices must have enough free RAM to be able to upload the new image to /tmp

1. Create a category

Create a category for your firmware images by going to Firmware management > Firmware categories > Add firmware category, if you use only one firmware type in your network, you could simply name the category "default" or "standard".

https://raw.githubusercontent.com/openwisp/openwisp-firmware-upgrader/docs/docs/images/quickstart-category.gif

If you use multiple firmware images with different features, create one category for each firmware type, eg:

  • WiFi
  • SDN router
  • LoRa Gateway

This is necessary in order to perform mass upgrades only on specific firmware categories when, for example, a new LoRa Gateway firmware becomes available.

2. Create the build object

Create a build a build object by going to Firmware management > Firmware builds > Add firmware build, the build object is related to a firmware category and is the collection of the different firmware images which have been compiled for the different hardware models supported by the system.

The version field indicates the firmware version, the change log field is optional but we recommend filling it to help operators know the differences between each version.

https://raw.githubusercontent.com/openwisp/openwisp-firmware-upgrader/docs/docs/images/quickstart-build.gif

An important but optional field of the build model is OS identifier, this field should match the value of the Operating System field which gets automatically filled during device registration, eg: OpenWrt 19.07-SNAPSHOT r11061-6ffd4d8a4d. It is used by the firmware-upgrader module to automatically create DeviceFirmware objects for existing devices or when new devices register. A DeviceFirmware object represent the relationship between a device and a firmware image, it basically tells us which firmware image is installed on the device.

To find out the exact value to use, you should either do a test flash on a device and register it to the system or you should inspect the firmware image by decompressing it and find the generated value in the firmware image.

If you're not sure about what OS identifier to use, just leave it empty, you can fill it later on when you find out.

Now save the build object to create it.

3. Upload images to the build

Now is time to add images to the build, we suggest adding one image at time. Alternatively the REST API can be used to automate this step.

https://raw.githubusercontent.com/openwisp/openwisp-firmware-upgrader/docs/docs/images/quickstart-firmwareimage.gif

If you use a hardware model which is not listed in the image types, if the hardware model is officially supported by OpenWRT, you can send us a pull-request to add it, otherwise you can use the setting OPENWISP_CUSTOM_OPENWRT_IMAGES to add it.

4. Perform a firmware upgrade to a specific device

https://raw.githubusercontent.com/openwisp/openwisp-firmware-upgrader/docs/docs/images/quickstart-devicefirmware.gif

Once a new build is ready, has been created in the system and its image have been uploaded, it will be the time to finally upgrade our devices.

To perform the upgrade of a single device, navigate to the device details, then go to the "Firmware" tab.

If you correctly filled OS identifier in step 2, you should have a situation similar to the one above: in this example, the device is using version 1.0 and we want to upgrade it to version 2.0, once the new firmware image is selected we just have to hit save, then a new tab will appear in the device page which allows us to see what's going on during the upgrade.

Right now, the update of the upgrade information is not asynchronous yet, so you will have to reload the page periodically to find new information. This will be addressed in a future release.

5. Performing mass upgrades

First of all, please ensure the following preconditions are met:

  • the system is configured correctly
  • the new firmware images are working as expected
  • you already tried the upgrade of single devices several times.

At this stage you can try a mass upgrade by doing the following:

  • go to the build list page
  • select the build which contains the latest firmware images you want the devices to be upgraded with
  • click on "Mass-upgrade devices related to the selected build".

https://raw.githubusercontent.com/openwisp/openwisp-firmware-upgrader/docs/docs/images/quickstart-batch-upgrade.gif

At this point you should see a summary page which will inform you of which devices are going to be upgraded, you can either confirm the operation or cancel.

Once the operation is confirmed you will be redirected to a page in which you can monitor the progress of the upgrade operations.

Right now, the update of the upgrade information is not asynchronous yet, so you will have to reload the page periodically to find new information. This will be addressed in a future release.

Automatic device firmware detection

OpenWISP Firmware Upgrader maintains a data structure for mapping the firmware image files to board names called OPENWRT_FIRMWARE_IMAGE_MAP.

Here is an example firmware image item from OPENWRT_FIRMWARE_IMAGE_MAP

{
    # Firmware image file name.
    'ar71xx-generic-cf-e320n-v2-squashfs-sysupgrade.bin': {
        # Human readable name of the model which is displayed on
        # the UI
        'label': 'COMFAST CF-E320N v2 (OpenWRT 19.07 and earlier)',
        # Tupe of board names with which the different versions
        # of the hardware are identified on OpenWrt
        'boards': ('COMFAST CF-E320N v2',),
    }
}

When a device registers on OpenWISP, the openwisp-config agent read the device board name from /tmp/sysinfo/model and sends it to OpenWISP. This value is then saved in the Device.model field. OpenWISP Firmware Upgrader uses this field to automatically detect the correct firmware image for the device.

Use the OPENWISP_CUSTOM_OPENWRT_IMAGES setting to add additional firmware image in your project.

Writing Custom Firmware Upgrader Classes

You can write custom upgraders for other firmware OSes or for custom OpenWrt based firmwares.

Here is an example custom OpenWrt firmware upgrader class:

from openwisp_firmware_upgrader.upgraders.openwrt import OpenWrt

class CustomOpenWrtBasedFirmware(OpenWrt):
    # this firmware uses a custom upgrade command
    UPGRADE_COMMAND = 'upgrade_firmware.sh --keep-config'
    # it takes somewhat more time to boot so it needs more time
    RECONNECT_DELAY = 150
    RECONNECT_RETRY_DELAY = 5
    RECONNECT_MAX_RETRIES = 20

    def get_remote_path(self, image):
        return '/tmp/firmware.img'

    def get_upgrade_command(self, path):
        return self.UPGRADE_COMMAND

You will need to place your custom upgrader class on the python path of your application and then add this path to the OPENWISP_FIRMWARE_UPGRADERS_MAP setting.

REST API

To enable the API the setting OPENWISP_FIRMWARE_UPGRADER_API must be set to True.

Live documentation

https://raw.githubusercontent.com/openwisp/openwisp-firmware-upgrader/docs/docs/images/api-docs.png

A general live API documentation (following the OpenAPI specification) at /api/v1/docs/.

Browsable web interface

https://raw.githubusercontent.com/openwisp/openwisp-firmware-upgrader/docs/docs/images/api-ui.png

Additionally, opening any of the endpoints listed below directly in the browser will show the browsable API interface of Django-REST-Framework, which makes it even easier to find out the details of each endpoint.

Authentication

See openwisp-users: authenticating with the user token.

When browsing the API via the Live documentation or the Browsable web page, you can also use the session authentication by logging in the django admin.

Pagination

All list endpoints support the page_size parameter that allows paginating the results in conjunction with the page parameter.

GET /api/v1/firmware-upgrader/build/?page_size=10
GET /api/v1/firmware-upgrader/build/?page_size=10&page=2

Filtering by organization slug

Most endpoints allow to filter by organization slug, eg:

GET /api/v1/firmware-upgrader/build/?organization=org-slug

List of endpoints

Since the detailed explanation is contained in the Live documentation and in the Browsable web page of each point, here we'll provide just a list of the available endpoints, for further information please open the URL of the endpoint in your browser.

List mass upgrade operations

GET /api/v1/firmware-upgrader/batch-upgrade-operation/

Available filters

The list of batch upgrade operations provides the following filters:

  • build (Firmware build ID)
  • status (One of: idle, in-progress, success, failed)

Here's a few examples:

GET /api/v1/firmware-upgrader/batch-upgrade-operation/?build={build_id}
GET /api/v1/firmware-upgrader/batch-upgrade-operation/?status={status}

Get mass upgrade operation detail

GET /api/v1/firmware-upgrader/batch-upgrade-operation/{id}/

List firmware builds

GET /api/v1/firmware-upgrader/build/

Available filters

The list of firmware builds provides the following filters:

  • category (Firmware category ID)
  • version (Firmware build version)
  • os (Firmware build os identifier)

Here's a few examples:

GET /api/v1/firmware-upgrader/build/?category={category_id}
GET /api/v1/firmware-upgrader/build/?version={version}
GET /api/v1/firmware-upgrader/build/?os={os}

Create firmware build

POST /api/v1/firmware-upgrader/build/

Get firmware build details

GET /api/v1/firmware-upgrader/build/{id}/

Change details of firmware build

PUT /api/v1/firmware-upgrader/build/{id}/

Patch details of firmware build

PATCH /api/v1/firmware-upgrader/build/{id}/

Delete firmware build

DELETE /api/v1/firmware-upgrader/build/{id}/

Get list of images of a firmware build

GET /api/v1/firmware-upgrader/build/{id}/image/

Available filters

The list of images of a firmware build can be filtered by using type (any one of the available firmware image types).

GET /api/v1/firmware-upgrader/build/{id}/image/?type={type}

Upload new firmware image to the build

POST /api/v1/firmware-upgrader/build/{id}/image/

Get firmware image details

GET /api/v1/firmware-upgrader/build/{build_id}/image/{id}/

Delete firmware image

DELETE /api/v1/firmware-upgrader/build/{build_id}/image/{id}/

Download firmware image

GET /api/v1/firmware-upgrader/build/{build_id}/image/{id}/download/

Perform batch upgrade

Upgrades all the devices related to the specified build ID.

POST /api/v1/firmware-upgrader/build/{id}/upgrade/

Dry-run batch upgrade

Returns a list representing the DeviceFirmware and Device instances that would be upgraded if POST is used.

Device objects are indicated only when no DeviceFirmware object exists for a device which would be upgraded.

GET /api/v1/firmware-upgrader/build/{id}/upgrade/

List firmware categories

GET /api/v1/firmware-upgrader/category/

Create new firmware category

POST /api/v1/firmware-upgrader/category/

Get firmware category details

GET /api/v1/firmware-upgrader/category/{id}/

Change the details of a firmware category

PUT /api/v1/firmware-upgrader/category/{id}/

Patch the details of a firmware category

PATCH /api/v1/firmware-upgrader/category/{id}/

Delete a firmware category

DELETE /api/v1/firmware-upgrader/category/{id}/

List upgrade operations

GET /api/v1/firmware-upgrader/upgrade-operation/

Available filters

The list of upgrade operations provides the following filters:

  • device__organization (Organization ID of the device)
  • device__organization_slug (Organization slug of the device)
  • device (Device ID)
  • image (Firmware image ID)
  • status (One of: in-progress, success, failed, aborted)

Here's a few examples:

GET /api/v1/firmware-upgrader/upgrade-operation/?device__organization={organization_id}
GET /api/v1/firmware-upgrader/upgrade-operation/?device__organization__slug={organization_slug}
GET /api/v1/firmware-upgrader/upgrade-operation/?device={device_id}
GET /api/v1/firmware-upgrader/upgrade-operation/?image={image_id}
GET /api/v1/firmware-upgrader/upgrade-operation/?status={status}

Get upgrade operation details

GET /api/v1/firmware-upgrader/upgrade-operation/{id}

List device upgrade operations

GET /api/v1/firmware-upgrader/device/{device_id}/upgrade-operation/

Available filters

The list of device upgrade operations can be filtered by status (one of: in-progress, success, failed, aborted).

GET /api/v1/firmware-upgrader/device/{device_id}/upgrade-operation/?status={status}

Create device firmware

Sending a PUT request to the endpoint below will create a new device firmware if it does not already exist.

PUT /api/v1/firmware-upgrader/device/{device_id}/firmware/

Get device firmware details

GET /api/v1/firmware-upgrader/device/{device_id}/firmware/

Change details of device firmware

PUT /api/v1/firmware-upgrader/device/{device_id}/firmware/

Patch details of device firmware

PATCH /api/v1/firmware-upgrader/device/{device_id}/firmware/

Delete device firmware

DELETE /api/v1/firmware-upgrader/device/{device_pk}/firmware/

Settings

OPENWISP_FIRMWARE_UPGRADER_RETRY_OPTIONS

type: dict
default: see below
# default value of OPENWISP_FIRMWARE_UPGRADER_RETRY_OPTIONS:

dict(
   max_retries=4,
   retry_backoff=60,
   retry_backoff_max=600,
   retry_jitter=True,
)

Retry settings for recoverable failures during firmware upgrades.

By default if an upgrade operation fails before the firmware is flashed (eg: because of a network issue during the upload of the image), the upgrade operation will be retried 4 more times with an exponential random backoff and a maximum delay of 10 minutes.

For more information regarding these settings, consult the celery documentation regarding automatic retries for known errors.

OPENWISP_FIRMWARE_UPGRADER_TASK_TIMEOUT

type: int
default: 600

Timeout for the background tasks which perform firmware upgrades.

If for some unexpected reason an upgrade remains stuck for more than 10 minutes, the upgrade operation will be flagged as failed and the task will be killed.

This should not happen, but a global task time out is a best practice when using background tasks because it prevents the situation in which an unexpected bug causes a specific task to hang, which will quickly fill all the available slots in a background queue and prevent other tasks from being executed, which will end up affecting negatively the rest of the application.

OPENWISP_CUSTOM_OPENWRT_IMAGES

type: tuple
default: None

This setting can be used to extend the list of firmware image types included in OpenWISP Firmware Upgrader. This setting is suited to add support for custom OpenWrt images.

OPENWISP_CUSTOM_OPENWRT_IMAGES = (
    (
        # Firmware image file name.
        'customimage-squashfs-sysupgrade.bin', {
            # Human readable name of the model which is displayed on
            # the UI
            'label': 'Custom WAP-1200',
            # Tuple of board names with which the different versions of
            # the hardware are identified on OpenWrt
            'boards': ('CWAP1200',)
        }
    ),
)

Kindly read "Automatic detection of firmware of device" section of this documentation to know how OpenWISP Firmware Upgrader uses this setting in upgrades.

OPENWISP_FIRMWARE_UPGRADER_MAX_FILE_SIZE

type: int
default: 30 * 1024 * 1024 (30 MB)

This setting can be used to set the maximum size limit for firmware images, eg:

OPENWISP_FIRMWARE_UPGRADER_MAX_FILE_SIZE = 40 * 1024 * 1024  # 40MB

Notes:

  • Value must be specified in bytes. None means unlimited.

OPENWISP_FIRMWARE_UPGRADER_API

type: bool
default: True

Indicates whether the API for Firmware Upgrader is enabled or not.

OPENWISP_FIRMWARE_UPGRADER_OPENWRT_SETTINGS

type: dict
default: {}

Allows changing the default OpenWRT upgrader settings, eg:

OPENWISP_FIRMWARE_UPGRADER_OPENWRT_SETTINGS = {
    'reconnect_delay': 120,
    'reconnect_retry_delay': 20,
    'reconnect_max_retries': 15,
    'upgrade_timeout': 90,
}
  • reconnect_delay: amount of seconds to wait before trying to connect again to the device after the upgrade command has been launched; the re-connection step is necessary to verify the upgrade has completed successfully; defaults to 120 seconds
  • reconnect_retry_delay: amount of seconds to wait after a re-connection attempt has failed; defaults to 20 seconds
  • reconnect_max_retries: maximum re-connection attempts defaults to 15 attempts
  • upgrade_timeout: amount of seconds before the shell session is closed after the upgrade command is launched on the device, useful in case the upgrade command hangs (it happens on older OpenWRT versions); defaults to 90 seconds

OPENWISP_FIRMWARE_API_BASEURL

type: dict
default: / (points to same server)

If you have a seperate instance of openwisp-firmware-upgrader API on a different domain, you can use this option to change the base of the image download url, this will enable you to point to your API server's domain, example value: https://myfirmware.myapp.com.

OPENWISP_FIRMWARE_UPGRADERS_MAP

type: dict
default:
{
  'openwisp_controller.connection.connectors.openwrt.ssh.OpenWrt': 'openwisp_firmware_upgrader.upgraders.openwrt.OpenWrt',
}

A dictionary that maps update strategies to upgraders.

If you want to use a custom update strategy you will need to use this setting to provide an entry with the class path of your update strategy as the key.

If you need to use a custom upgrader class you will need to use this setting to provide an entry with the class path of your upgrader as the value.

OPENWISP_FIRMWARE_PRIVATE_STORAGE_INSTANCE

type: str
default: openwisp_firmware_upgrader.private_storage.storage.file_system_private_storage

Dotted path to an instance of any one of the storage classes in private_storage. This instance is used to store firmware image files.

By default, an instance of private_storage.storage.files.PrivateFileSystemStorage is used.

Extending openwisp-firmware-upgrader

One of the core values of the OpenWISP project is Software Reusability, for this reason OpenWISP Firmware Upgrader provides a set of base classes which can be imported, extended and reused to create derivative apps.

In order to implement your custom version of OpenWISP Firmware Upgrader, you need to perform the steps described in this section.

When in doubt, the code in the test project and the sample app will serve you as source of truth: just replicate and adapt that code to get a basic derivative of OpenWISP Firmware Upgrader working.

Premise: if you plan on using a customized version of this module, we suggest to start with it since the beginning, because migrating your data from the default module to your extended version may be time consuming.

1. Initialize your custom module

The first thing you need to do is to create a new django app which will contain your custom version of OpenWISP Firmware Upgrader.

A django app is nothing more than a python package (a directory of python scripts), in the following examples we'll call this django app myupgrader, but you can name it how you want:

django-admin startapp myupgrader

Keep in mind that the command mentioned above must be called from a directory which is available in your PYTHON_PATH so that you can then import the result into your project.

Now you need to add myupgrader to INSTALLED_APPS in your settings.py, ensuring also that openwisp_firmware_upgrader has been removed:

INSTALLED_APPS = [
    # ... other apps ...

    # 'openwisp_firmware_upgrader'  <-- comment out or delete this line
    'myupgrader'
]

For more information about how to work with django projects and django apps, please refer to the django documentation.

2. Install openwisp-firmware-upgrader

Install (and add to the requirement of your project) openwisp-firmware-upgrader:

pip install openwisp-firmware-upgrader

3. Add EXTENDED_APPS

Add the following to your settings.py:

EXTENDED_APPS = ['openwisp_firmware_upgrader']

4. Add openwisp_utils.staticfiles.DependencyFinder

Add openwisp_utils.staticfiles.DependencyFinder to STATICFILES_FINDERS in your settings.py:

STATICFILES_FINDERS = [
    'django.contrib.staticfiles.finders.FileSystemFinder',
    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
    'openwisp_utils.staticfiles.DependencyFinder',
]

5. Add openwisp_utils.loaders.DependencyLoader

Add openwisp_utils.loaders.DependencyLoader to TEMPLATES in your settings.py:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'OPTIONS': {
            'loaders': [
                'django.template.loaders.filesystem.Loader',
                'django.template.loaders.app_directories.Loader',
                'openwisp_utils.loaders.DependencyLoader',
            ],
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    }
]

6. Inherit the AppConfig class

Please refer to the following files in the sample app of the test project:

You have to replicate and adapt that code in your project.

For more information regarding the concept of AppConfig please refer to the "Applications" section in the django documentation.

7. Create your custom models

For the purpose of showing an example, we added a simple "details" field to the models of the sample app in the test project.

You can add fields in a similar way in your models.py file.

Note: for doubts regarding how to use, extend or develop models please refer to the "Models" section in the django documentation.

8. Add swapper configurations

Once you have created the models, add the following to your settings.py:

# Setting models for swapper module
FIRMWARE_UPGRADER_CATEGORY_MODEL = 'myupgrader.Category'
FIRMWARE_UPGRADER_BUILD_MODEL = 'myupgrader.Build'
FIRMWARE_UPGRADER_FIRMWAREIMAGE_MODEL = 'myupgrader.FirmwareImage'
FIRMWARE_UPGRADER_DEVICEFIRMWARE_MODEL = 'myupgrader.DeviceFirmware'
FIRMWARE_UPGRADER_BATCHUPGRADEOPERATION_MODEL = 'myupgrader.BatchUpgradeOperation'
FIRMWARE_UPGRADER_UPGRADEOPERATION_MODEL = 'myupgrader.UpgradeOperation'

Substitute myupgrader with the name you chose in step 1.

9. Create database migrations

Create and apply database migrations:

./manage.py makemigrations
./manage.py migrate

For more information, refer to the "Migrations" section in the django documentation.

10. Create the admin

Refer to the admin.py file of the sample app.

To introduce changes to the admin, you can do it in two main ways which are described below.

Note: for more information regarding how the django admin works, or how it can be customized, please refer to "The django admin site" section in the django documentation.

1. Monkey patching

If the changes you need to add are relatively small, you can resort to monkey patching.

For example:

from openwisp_firmware_upgrader.admin import (  # noqa
    BatchUpgradeOperationAdmin,
    BuildAdmin,
    CategoryAdmin,
)

BuildAdmin.list_display.insert(1, 'my_custom_field')
BuildAdmin.ordering = ['-my_custom_field']

2. Inheriting admin classes

If you need to introduce significant changes and/or you don't want to resort to monkey patching, you can proceed as follows:

from django.contrib import admin
from openwisp_firmware_upgrader.admin import (
    BatchUpgradeOperationAdmin as BaseBatchUpgradeOperationAdmin,
    BuildAdmin as BaseBuildAdmin,
    CategoryAdmin as BaseCategoryAdmin,
)
from openwisp_firmware_upgrader.swapper import load_model

BatchUpgradeOperation = load_model('BatchUpgradeOperation')
Build = load_model('Build')
Category = load_model('Category')
DeviceFirmware = load_model('DeviceFirmware')
FirmwareImage = load_model('FirmwareImage')
UpgradeOperation = load_model('UpgradeOperation')

admin.site.unregister(BatchUpgradeOperation)
admin.site.unregister(Build)
admin.site.unregister(Category)

class BatchUpgradeOperationAdmin(BaseBatchUpgradeOperationAdmin):
    # add your changes here

class BuildAdmin(BaseBuildAdmin):
    # add your changes here

class CategoryAdmin(BaseCategoryAdmin):
    # add your changes here

11. Create root URL configuration

Please refer to the urls.py file in the test project.

For more information about URL configuration in django, please refer to the "URL dispatcher" section in the django documentation.

12. Create celery.py

Please refer to the celery.py file in the test project.

For more information about the usage of celery in django, please refer to the "First steps with Django" section in the celery documentation.

13. Import the automated tests

When developing a custom application based on this module, it's a good idea to import and run the base tests too, so that you can be sure the changes you're introducing are not breaking some of the existing features of OpenWISP Firmware Upgrader.

In case you need to add breaking changes, you can overwrite the tests defined in the base classes to test your own behavior.

See the tests of the sample app to find out how to do this.

You can then run tests with:

# the --parallel flag is optional
./manage.py test --parallel myupgrader

Substitute myupgrader with the name you chose in step 1.

For more information about automated tests in django, please refer to "Testing in Django".

Other base classes that can be inherited and extended

The following steps are not required and are intended for more advanced customization.

FirmwareImageDownloadView

This view controls how the firmware images are stored and who has permission to download them.

The full python path is: openwisp_firmware_upgrader.private_storage.FirmwareImageDownloadView.

If you want to extend this view, you will have to perform the additional steps below.

Step 1. import and extend view:

# myupgrader/views.py
from openwisp_firmware_upgrader.private_storage import (
    FirmwareImageDownloadView as BaseFirmwareImageDownloadView
)

class FirmwareImageDownloadView(BaseFirmwareImageDownloadView):
    # add your customizations here ...
    pass

Step 2: remove the following line from your root urls.py file:

path('firmware/', include('openwisp_firmware_upgrader.private_storage.urls')),

Step 3: add an URL route pointing to your custom view in urls.py file:

# urls.py
from myupgrader.views import FirmwareImageDownloadView

urlpatterns = [
    # ... other URLs
    path('<your-custom-path>', FirmwareImageDownloadView.as_view(), name='serve_private_file',),
]

For more information regarding django views, please refer to the "Class based views" section in the django documentation.

API views

If you need to customize the behavior of the API views, the procedure to follow is similar to the one described in FirmwareImageDownloadView, with the difference that you may also want to create your own serializers if needed.

The API code is stored in openwisp_firmware_upgrader.api and is built using django-rest-framework

For more information regarding Django REST Framework API views, please refer to the "Generic views" section in the Django REST Framework documentation.

Contributing

Please refer to the OpenWISP contributing guidelines.

Support

See OpenWISP Support Channels.

Changelog

See CHANGES.

License

See LICENSE.

openwisp-firmware-upgrader's People

Contributors

abhigyashridhar avatar aryamanz29 avatar atb00ker avatar c4llous avatar codesankalp avatar daffytheduck avatar darecoder avatar devkapilbansal avatar hanif-ali avatar kosli avatar manishshah120 avatar mohzulfikar avatar momothefox avatar nemesifier avatar nepython avatar niteshsinha17 avatar noumbissivalere avatar okraits avatar pablocastellano avatar pandafy avatar praptisharma28 avatar priyanshuone6 avatar purhan avatar r9295 avatar theminus avatar yashikajotwani12 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

openwisp-firmware-upgrader's Issues

[fw-upgrader] Document upgraders

Briefly document the concept of upgrader class and show an example of how to implement a custom upgrader.

This is an example of a custom upgrader:

from openwisp_firmware_upgrader.upgraders.openwrt import OpenWrt


class CustomOpenWrtBasedFirmware(OpenWrt):
    # this firmware uses a custom upgrade command
    UPGRADE_COMMAND = 'upgrade_firmware.sh --keep-config'
    # it takes somewhat more time to boot so it needs more time
    RECONNECT_DELAY = 150
    RECONNECT_RETRY_DELAY = 5
    RECONNECT_MAX_RETRIES = 20

    def get_remote_path(self, image):
        return '/tmp/firmware.img'

    def get_upgrade_command(self, path):
        return self.UPGRADE_COMMAND

[change] Improve endpoints to download firmware images

Right now, the API endpoint which shows the image information, has a field, called "file", which links to the private-storage view.
But we also have an API endpoint to download the firmware, although it's implemented differently than the private storage view.

It also looks that the private storage view does not check if the user has "view permission" on the object. So it would be theoretically possible that a staff user without premissions to view firmware images can still download the firmware image, which is a problem.

Therefore, I think we should do the following things:

  • Find a way to call openwisp_firmware_upgrader.private_storage.views.FirmwareImageDownloadView from openwisp_firmware_upgrader.api.views.FirmwareImageDownloadView, to avoid duplicating logic
  • Change the file attribute of FirmwareImageSerializer to point to the API URL (because API supports token authentication (we need a test that ensures the new URL is not the one generated by private_storage)
  • Ensure the private storage view checks whether the user has the view permission on FirmwareImage objects (we need a test for this), once this is done, we can try to set permission_classes to an empty list on the API FirmwareImageDownloadView, to avoid checking permissions twice, in theory it should work

[fw-upgrade] Error in batch upgrade operations list

Similar procedure to #16 but no need to create device.

FieldError: Related Field got invalid lookup: organization
  File "django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "django/core/handlers/base.py", line 115, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "django/core/handlers/base.py", line 113, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "django/contrib/admin/options.py", line 607, in wrapper
    return self.admin_site.admin_view(view)(*args, **kwargs)
  File "django/utils/decorators.py", line 130, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "django/views/decorators/cache.py", line 44, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File "django/contrib/admin/sites.py", line 231, in inner
    return view(request, *args, **kwargs)
  File "django/utils/decorators.py", line 43, in _wrapper
    return bound_method(*args, **kwargs)
  File "django/utils/decorators.py", line 130, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "django/contrib/admin/options.py", line 1678, in changelist_view
    cl = self.get_changelist_instance(request)
  File "django/contrib/admin/options.py", line 741, in get_changelist_instance
    sortable_by,
  File "django/contrib/admin/views/main.py", line 57, in __init__
    self.root_queryset = model_admin.get_queryset(request)
  File "openwisp_users/multitenancy.py", line 46, in get_queryset
    return qs.filter(**{qsarg: user.organizations_pk})
  File "django/db/models/query.py", line 904, in filter
    return self._filter_or_exclude(False, *args, **kwargs)
  File "django/db/models/query.py", line 923, in _filter_or_exclude
    clone.query.add_q(Q(*args, **kwargs))
  File "django/db/models/sql/query.py", line 1350, in add_q
    clause, _ = self._add_q(q_object, self.used_aliases)
  File "django/db/models/sql/query.py", line 1381, in _add_q
    check_filterable=check_filterable,
  File "django/db/models/sql/query.py", line 1303, in build_filter
    raise FieldError('Related Field got invalid lookup: {}'.format(lookups[0]))

[firmware-upgrader] Set up travis build and qa checks

Set up a travis build in line with the other openwisp modules (see django-freeradius, openwisp-ipam, for recent good examples).

The build should test the latest versions of django (2.2 and 3.0) and perform all the qa-checks done in the other repos.

[qa] Write admin tests which check the presence of details field in admin UI

The sample app models have a details field which is shown in the admin:

class DetailsModel(models.Model):
details = models.CharField(max_length=64, blank=True, null=True)
class Meta:
abstract = True

But we don't have automated tests for this.

If this feature breaks, we won't know from tests.

Write additional tests in tests/openwisp2/sample_firmware_upgrader/tests.py (TestAdmin) to check this.

One test for each admin class being used.

[api] Add REST API

Depends on openwisp/openwisp-users#104.

This is an optional feature which should be disabled by default, the setting to use shall be OPENWISP_FIRMWARE_UPGRADER_API.

Add django-rest-framework in the extra_requires['rest'] of setup.py.

Add the API endpoints to perform the following operations, they shall use the TokenAuthentication defined in openwisp-users and use the DjangoPermission set and users shall be able to access only the objects they're authorized to see based on their organization (superusers can access everything, staff users can access only objects related to organizations they're member of and flagged as is_admin):

  • Build:
    • view list of builds (possibility to filter by org)
    • add (should be possible to upload firmware images as well)
    • change (should be possible to add/change/remove firmware images to the build as well)
    • delete
  • Category
    • view (possibility to filter by org)
    • add
    • change
    • delete
  • Mass upgrade operations
    • view (possibility to filter by org)
    • run mass upgrade

[fw-upgrade] Create a new permission to launch upgrades

  • create a new permission to launch single upgrades
  • create a new permission to launch mass upgrades

It may not be necessary to create a new permission, maybe we can get away with the permissions of the models that already exist (UpgradeOperation and MassUpgradeOperation) but we need to verify this.

If we need to create the new permissions, ensure they are automatically added to the administrator group (see #15).

[feature] Automatically create DeviceFirmware instances

When a device registers it will have no DeviceFirmware instance associated, which makes it cumbersome to upgrade (because it must be created from scratch and some users may not know what to do, while if it's already there, changing it is very easy).

Here's a simple way we could create it automatically.

All devices using the same firmware build will have the same content in the "operating system" field of the device.

We can add a new field to Build, something like os_identifier, where users have to place what comes up after devices register (users have to find out by extracting the firmware image with binwalk or by flashing a device).

When a device registers (post_save signal of Device with created argument True), we can check whether there's any Build for the same organization which has that os identifier and if there's a firmware image compatible with the model of the device, if there is, we can create the DeviceFirmware intance (making sure to not try to trigger an upgrade by using save(upgrade=False) when saving the FirmwareImage instance).

The opeartion should skip if the os and model fields are empty.

The receiver function should be connected in the ready() method of the AppConfig, like we do in openwisp-monitoring.

[fw-upgrader] Admin: 500 error when saving a device (corner case)

How to replicate:

  1. add firmware build, add an image to it (use any file)
  2. open another browser tab, add a device, save and continue, go to the "Device Firmware" tab, select the image created in the previous step, hit "save and continue", keep this browser tab open
  3. in another browser tab, remove the firmware build created in step 1
  4. in the browser tab left open in step 2, hit "save" again

You should see an exception like:

Traceback (most recent call last):
  File "/python3.7/site-packages/django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "/python3.7/site-packages/django/core/handlers/base.py", line 115, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/python3.7/site-packages/django/core/handlers/base.py", line 113, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/python3.7/site-packages/django/contrib/admin/options.py", line 607, in wrapper
    return self.admin_site.admin_view(view)(*args, **kwargs)
  File "/python3.7/site-packages/django/utils/decorators.py", line 130, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "/python3.7/site-packages/django/views/decorators/cache.py", line 44, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File "/python3.7/site-packages/django/contrib/admin/sites.py", line 231, in inner
    return view(request, *args, **kwargs)
  File "/django-netjsonconfig/django_netjsonconfig/base/admin.py", line 87, in change_view
    return super().change_view(request, object_id, form_url, extra_context)
  File "/python3.7/site-packages/django_reversion-3.0.5-py3.7.egg/reversion/admin.py", line 154, in change_view
    return super().change_view(request, object_id, form_url, extra_context)
  File "/python3.7/site-packages/django/contrib/admin/options.py", line 1641, in change_view
    return self.changeform_view(request, object_id, form_url, extra_context)
  File "/python3.7/site-packages/django/utils/decorators.py", line 43, in _wrapper
    return bound_method(*args, **kwargs)
  File "/python3.7/site-packages/django/utils/decorators.py", line 130, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "/python3.7/site-packages/django/contrib/admin/options.py", line 1522, in changeform_view
    return self._changeform_view(request, object_id, form_url, extra_context)
  File "/python3.7/site-packages/django/contrib/admin/options.py", line 1564, in _changeform_view
    if all_valid(formsets) and form_validated:
  File "/python3.7/site-packages/django/forms/formsets.py", line 464, in all_valid
    valid &= formset.is_valid()
  File "/python3.7/site-packages/django/forms/formsets.py", line 308, in is_valid
    self.errors
  File "/python3.7/site-packages/django/forms/formsets.py", line 288, in errors
    self.full_clean()
  File "/python3.7/site-packages/django/forms/formsets.py", line 336, in full_clean
    form_errors = form.errors
  File "/python3.7/site-packages/django/forms/forms.py", line 175, in errors
    self.full_clean()
  File "/python3.7/site-packages/django/forms/forms.py", line 378, in full_clean
    self._post_clean()
  File "/python3.7/site-packages/django/forms/models.py", line 404, in _post_clean
    self.instance.full_clean(exclude=exclude, validate_unique=False)
  File "/python3.7/site-packages/django/db/models/base.py", line 1207, in full_clean
    self.clean()
  File "/openwisp-firmware-upgrader/openwisp_firmware_upgrader/base/models.py", line 240, in clean
    if self.image.build.category.organization != self.device.organization:
  File "/python3.7/site-packages/django/db/models/fields/related_descriptors.py", line 197, in __get__
    "%s has no %s." % (self.field.model.__name__, self.field.name)

Exception Type: RelatedObjectDoesNotExist at /admin/config/device/a8ec32d4-3537-4c97-afd4-08abc57d465b/change/
Exception Value: DeviceFirmware has no image.
  • Replicate issue
  • Add failing test
  • Implement fix and send a pull-request

[admin] Show in progress upgrade operation in Device Firmware tab

Not super obvious, but it's possible to launch single upgrade operations for each device from the device page, using the Device Firmware tab (selecting a firmware image and saving).

First of all, we should order the images so the latest images come first.
We should also filter the image to show only the images for the type of hardware being used in that device (if the information is available).

However if the single upgrade opration is launched this way, there's no way to track its progress.

A simple solution is to look if an UpgradeOperation related to that device is in progress and if yes, show its details there.

[admin] upgrade_related parameter is not used

upgrade_related = request.POST.get('upgrade_related')
build = queryset.first()
# upgrade has been confirmed
if upgrade_all or upgrade_related:
batch = build.batch_upgrade(firmwareless=upgrade_all)
text = _(
'You can track the progress of this mass upgrade operation '
'in this page. Refresh the page from time to time to check '
'its progress.'
)
self.message_user(request, mark_safe(text), messages.SUCCESS)
url = reverse(
f'admin:{app_label}_batchupgradeoperation_change', args=[batch.pk]
)
return redirect(url)

This is the logic simplified:

upgrade_all = request.POST.get('upgrade_all')
upgrade_related = request.POST.get('upgrade_related') 
if upgrade_all or upgrade_related:
    batch = build.batch_upgrade(firmwareless=upgrade_all)

Should we remove it?

Originally posted by @PabloCastellano in #73

[docs] Add firmware-upgrader quickstart

Create a build, explain categories, explain images, point out how to add custom firmware imags, explain OS identifier, point out list of available firmware images is incomplete.

[api] Make firmware image not editable

@PabloCastellano see this: #44

I think we can make the firmware images not editable.
Users will have to delete and recreate it.

This makes several things easier and also makes sense since we want to avoid the situation in which all devices are flashed with an image, then the image file is changed with a different one but the system still believes the devices are using that image (they're not, they'll be using a different one).
So forcing the users to delete the image if they want to change it, also makes the system aware of this.

I think we can do this in the API (we'll do it also in the admin in #44).

This means you can remove this:

# FIXME: I'm unable to get the test working
"""
def test_firmware_update(self):
image = self._create_firmware_image()
url = reverse('upgrader:api_firmware_detail', args=[image.build.pk ,image.pk])
data = {
"type": self.TPLINK_4300_IL_IMAGE,
"file": self._get_simpleuploadedfile_multipart(),
}
#r = self.client.put(url, data, content_disposition="attachment;
filename=f'openwrt-{self.TPLINK_4300_IMAGE}'")
r = self.client.put(url, data, content_type='multipart/form-data')
import ipdb; ipdb.set_trace()
self.assertEqual(r.data["id"], str(image.pk))
self.assertEqual(r.data["build"], image.build.pk)
self.assertEqual(r.data["type"], self.TPLINK_4300_IL_IMAGE)
"""

And its related functionality.

[fw-upgrade] Save log lines gradually

Right now the log lines are saved at the end of an upgrade operation.
It's a lot more useful if the lines are saved as soon as they're received from the device.

We need to pass the DeviceConnection intance to the upgrader class, then we can reuse the DeviceConnection methods like connect(), disconnect() (to reduce repetition) and save().

[fw-upgrade] Error in DeviceAdmin caused by DeviceFirmware

How to replicate:

  • create a a user which is not superuser and has access to the default org
  • create a device
  • create a device firmware for the device
  • add the permissions to the objects of this module to the administrator group (because these are not added by default yet, another issue is already open to solve this #15).
  • open the device page

Stacktrace:

FieldError: Cannot resolve keyword 'organization' into field. Choices are: build, build_id, created, devicefirmware, file, id, modified, type, upgradeoperation
  File "django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "django/core/handlers/base.py", line 115, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "django/core/handlers/base.py", line 113, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "django/contrib/admin/options.py", line 607, in wrapper
    return self.admin_site.admin_view(view)(*args, **kwargs)
  File "django/utils/decorators.py", line 130, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "django/views/decorators/cache.py", line 44, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File "django/contrib/admin/sites.py", line 231, in inner
    return view(request, *args, **kwargs)
  File "django_netjsonconfig/base/admin.py", line 87, in change_view
    return super().change_view(request, object_id, form_url, extra_context)
  File "reversion/admin.py", line 154, in change_view
    return super().change_view(request, object_id, form_url, extra_context)
  File "django/contrib/admin/options.py", line 1641, in change_view
    return self.changeform_view(request, object_id, form_url, extra_context)
  File "django/utils/decorators.py", line 43, in _wrapper
    return bound_method(*args, **kwargs)
  File "django/utils/decorators.py", line 130, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "django/contrib/admin/options.py", line 1522, in changeform_view
    return self._changeform_view(request, object_id, form_url, extra_context)
  File "django/contrib/admin/options.py", line 1583, in _changeform_view
    formsets, inline_instances = self._create_formsets(request, obj, change=True)
  File "django/contrib/admin/options.py", line 1938, in _create_formsets
    for FormSet, inline in self.get_formsets_with_inlines(*get_formsets_args):
  File "django/contrib/admin/options.py", line 791, in get_formsets_with_inlines
    yield inline.get_formset(request, obj), inline
  File "openwisp_users/multitenancy.py", line 81, in get_formset
    formset = super().get_formset(request, obj=None, **kwargs)
  File "django/contrib/admin/options.py", line 2037, in get_formset
    fields = flatten_fieldsets(self.get_fieldsets(request, obj))
  File "django/contrib/admin/options.py", line 328, in get_fieldsets
    return [(None, {'fields': self.get_fields(request, obj)})]
  File "django/contrib/admin/options.py", line 319, in get_fields
    form = self._get_form_for_get_fields(request, obj)
  File "django/contrib/admin/options.py", line 2122, in _get_form_for_get_fields
    return self.get_formset(request, obj, fields=None).form
  File "openwisp_users/multitenancy.py", line 82, in get_formset
    self._edit_form(request, formset.form)
  File "openwisp_users/multitenancy.py", line 73, in _edit_form
    field.queryset = field.queryset.filter(q)
  File "django/db/models/query.py", line 904, in filter
    return self._filter_or_exclude(False, *args, **kwargs)
  File "django/db/models/query.py", line 923, in _filter_or_exclude
    clone.query.add_q(Q(*args, **kwargs))
  File "django/db/models/sql/query.py", line 1350, in add_q
    clause, _ = self._add_q(q_object, self.used_aliases)
  File "django/db/models/sql/query.py", line 1373, in _add_q
    check_filterable,
  File "django/db/models/sql/query.py", line 1381, in _add_q
    check_filterable=check_filterable,
  File "django/db/models/sql/query.py", line 1250, in build_filter
    lookups, parts, reffed_expression = self.solve_lookup_type(arg)
  File "django/db/models/sql/query.py", line 1087, in solve_lookup_type
    _, field, _, lookup_parts = self.names_to_path(lookup_splitted, self.get_meta())
  File "django/db/models/sql/query.py", line 1483, in names_to_path
    "Choices are: %s" % (name, ", ".join(available)))

[fw-upgrade] Make upgrade operation more robust

An upgrade operation can fail for various and unexpected reasons.

At the moment, when an upgrade fails, we only know it has failed and with which error.

We must aim to achieve the following goals:

  • we must know if the upgrade failed before the command to reflash the device has been sent or afterwards, so AbstractUpgradeOperation should have a status for one case and another status for the other case
  • we have to ensure the AbortedOperation exception is raised when it's useless to repeat the operation
  • we have to create other dedicated exceptions for the other error cases
  • when we catch exceptions that signal the upgrade has failed before the reflash of the device, and the exception is not AbortedOperation, it means we can retry the upgrade again. Maybe the upload failed for a temporary network issue. We can retry 4 more times by default, this number should be cofigurable. The wait between each retry should be of 2 minutes. The celery options to use should be autoretry_for, max_retries and default_retry_delay: https://docs.celeryproject.org/en/stable/userguide/tasks.html#automatic-retry-for-known-exceptions

[api] User spanning multiple orgs crashes the API

Here's the issue:

if not self.request.user.is_superuser:
user_org = self.request.user.openwisp_users_organization.get()
organization_filter = {self.organization_field: user_org}
queryset = queryset.filter(**organization_filter)

Instead of expecting one org, we should expect multiple ones, the query should be something like organization__in=[org1_uuid, org2_uuid]

PS: we need to write some tests using an user which is not superuser, it seems this use case is broken.

[firmware-upgrader] Abstract and Swappable models

  • Extract the base logic of the models into abstract models
  • Set up django swappable models like we do in django-ipam / django-freeradius
  • Set up a sample_app for testing custom apps extending the models, like we do in django-ipam / openwisp-ipam

[upload] Store images in private location

Right now firmware images are stored in the media folder which is publicly accessible via unauthenticated HTTP (provided one knows the exact URL), we have to find a way to achieve the following:

  • firmware images have to be stored in a directory which is not accessible via unauthenticated HTTP requests
  • in order to download a file the user must be either superuser or must be a staff user with access to the organization
  • if possible, we should put all images of a build in a dedicated directory, this also avoids name collisions, it seem that now images are all saved in one directory, which will most probably cause name collisions

This open source package should help us in achieving this more quickly: https://github.com/edoburu/django-private-storage

[fw-upgrade] Remove build dir if empty

Since #20, files will be uploaded in a subdirectory which uses the build PK as its name.

Now when files are deleted, empty build directories remain (visible after running tests as well).

We need to update this method to handle this case:

def _remove_file(self):
path = self.file.path
if os.path.isfile(path):
os.remove(path)
else:
msg = 'firmware image not found while deleting {0}:\n{1}'
logger.error(msg.format(self, path))

I'd check whether the directory containing the file to remove is named as the build.pk and if yes and if the directory is empty, remove the directory as well.

[admin] Redirect to mass upgrade operation page after launch

After launching the mass upgrade operation in the admin, the system should directly redirect to its page where users can track the progress of the upgrade.

This will require changing a bit of model code, because right now the BatchUpgradeOperation instance is created in the celery task which launches the upgrade, while we need to create the object before and then only trigger the mass upgrade in the celery task, by passing the primary key of the BatchUpgradeOperation object just created; this way we'll know the id of the BatchUpgradeOperation and we'll be able to redirect to its page.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.