Jelajahi Sumber

Merge branch 'develop' into feature

jeremystretch 4 tahun lalu
induk
melakukan
efb41b7433

+ 1 - 1
.github/ISSUE_TEMPLATE/bug_report.yaml

@@ -17,7 +17,7 @@ body:
         What version of NetBox are you currently running? (If you don't have access to the most
         recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
         before opening a bug report to see if your issue has already been addressed.)
-      placeholder: v3.0.7
+      placeholder: v3.0.8
     validations:
       required: true
   - type: dropdown

+ 1 - 1
.github/ISSUE_TEMPLATE/feature_request.yaml

@@ -14,7 +14,7 @@ body:
     attributes:
       label: NetBox version
       description: What version of NetBox are you currently running?
-      placeholder: v3.0.7
+      placeholder: v3.0.8
     validations:
       required: true
   - type: dropdown

+ 0 - 1
docs/customization/custom-links.md

@@ -1 +0,0 @@
-{!models/extras/customlink.md!}

+ 22 - 1
docs/release-notes/version-3.0.md

@@ -1,6 +1,27 @@
 # NetBox v3.0
 
-## v3.0.8 (FUTURE)
+## v3.0.9 (FUTURE)
+
+---
+
+## v3.0.8 (2021-10-20)
+
+### Enhancements
+
+* [#7551](https://github.com/netbox-community/netbox/issues/7551) - Add UI field to filter interfaces by kind
+* [#7561](https://github.com/netbox-community/netbox/issues/7561) - Add a utilization column to the IP ranges table
+
+### Bug Fixes
+
+* [#7300](https://github.com/netbox-community/netbox/issues/7300) - Fix incorrect Device LLDP interface row coloring
+* [#7495](https://github.com/netbox-community/netbox/issues/7495) - Fix navigation UI issue that caused improper element overlap
+* [#7529](https://github.com/netbox-community/netbox/issues/7529) - Restore horizontal scrolling for tables in narrow viewports
+* [#7534](https://github.com/netbox-community/netbox/issues/7534) - Avoid exception when utilizing "create and add another" twice in succession
+* [#7544](https://github.com/netbox-community/netbox/issues/7544) - Fix multi-value filtering of custom field objects
+* [#7545](https://github.com/netbox-community/netbox/issues/7545) - Fix incorrect display of update/delete events for webhooks
+* [#7550](https://github.com/netbox-community/netbox/issues/7550) - Fix rendering of UTF8-encoded data in change records
+* [#7556](https://github.com/netbox-community/netbox/issues/7556) - Fix display of version when new release is available
+* [#7584](https://github.com/netbox-community/netbox/issues/7584) - Fix alignment of object identifier under object view
 
 ---
 

+ 1 - 1
mkdocs.yml

@@ -66,7 +66,7 @@ nav:
     - Customization:
         - Custom Fields: 'customization/custom-fields.md'
         - Custom Validation: 'customization/custom-validation.md'
-        - Custom Links: 'customization/custom-links.md'
+        - Custom Links: 'models/extras/customlink.md'
         - Export Templates: 'customization/export-templates.md'
         - Custom Scripts: 'customization/custom-scripts.md'
         - Reports: 'customization/reports.md'

+ 12 - 0
netbox/dcim/choices.py

@@ -704,6 +704,18 @@ class PowerOutletFeedLegChoices(ChoiceSet):
 # Interfaces
 #
 
+class InterfaceKindChoices(ChoiceSet):
+    KIND_PHYSICAL = 'physical'
+    KIND_VIRTUAL = 'virtual'
+    KIND_WIRELESS = 'wireless'
+
+    CHOICES = (
+        (KIND_PHYSICAL, 'Physical'),
+        (KIND_VIRTUAL, 'Virtual'),
+        (KIND_WIRELESS, 'Wireless'),
+    )
+
+
 class InterfaceTypeChoices(ChoiceSet):
 
     # Virtual

+ 6 - 2
netbox/dcim/forms/filtersets.py

@@ -7,7 +7,6 @@ from dcim.constants import *
 from dcim.models import *
 from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm
 from tenancy.forms import TenancyFilterForm
-from tenancy.models import Tenant
 from utilities.forms import (
     APISelectMultiple, add_blank_choice, BootstrapMixin, ColorField, DynamicModelMultipleChoiceField, StaticSelect,
     StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
@@ -966,9 +965,14 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
     model = Interface
     field_groups = [
         ['q', 'tag'],
-        ['name', 'label', 'type', 'enabled', 'mgmt_only', 'mac_address', 'wwn'],
+        ['name', 'label', 'kind', 'type', 'enabled', 'mgmt_only', 'mac_address', 'wwn'],
         ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
     ]
+    kind = forms.MultipleChoiceField(
+        choices=InterfaceKindChoices,
+        required=False,
+        widget=StaticSelectMultiple()
+    )
     type = forms.MultipleChoiceField(
         choices=InterfaceTypeChoices,
         required=False,

+ 2 - 1
netbox/extras/filtersets.py

@@ -15,6 +15,7 @@ from .models import *
 __all__ = (
     'ConfigContextFilterSet',
     'ContentTypeFilterSet',
+    'CustomFieldFilterSet',
     'CustomLinkFilterSet',
     'ExportTemplateFilterSet',
     'ImageAttachmentFilterSet',
@@ -47,7 +48,7 @@ class WebhookFilterSet(BaseFilterSet):
         ]
 
 
-class CustomFieldFilterSet(django_filters.FilterSet):
+class CustomFieldFilterSet(BaseFilterSet):
     content_types = ContentTypeFilter()
 
     class Meta:

+ 5 - 0
netbox/ipam/tables/ip.py

@@ -260,11 +260,16 @@ class IPRangeTable(BaseTable):
         linkify=True
     )
     tenant = TenantColumn()
+    utilization = UtilizationColumn(
+        accessor='utilization',
+        orderable=False
+    )
 
     class Meta(BaseTable.Meta):
         model = IPRange
         fields = (
             'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
+            'utilization',
         )
         default_columns = (
             'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',

+ 1 - 1
netbox/netbox/settings.py

@@ -16,7 +16,7 @@ from django.core.validators import URLValidator
 # Environment setup
 #
 
-VERSION = '3.0.8-dev'
+VERSION = '3.0.9-dev'
 
 # Hostname
 HOSTNAME = platform.node()

+ 1 - 1
netbox/netbox/views/__init__.py

@@ -137,7 +137,7 @@ class HomeView(View):
                 release_version, release_url = latest_release
                 if release_version > version.parse(settings.VERSION):
                     new_release = {
-                        'version': str(latest_release),
+                        'version': str(release_version),
                         'url': release_url,
                     }
 

+ 2 - 2
netbox/netbox/views/generic.py

@@ -282,11 +282,11 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
                 messages.success(request, mark_safe(msg))
 
                 if '_addanother' in request.POST:
-                    redirect_url = request.get_full_path()
+                    redirect_url = request.path
 
                     # If the object has clone_fields, pre-populate a new instance of the form
                     if hasattr(obj, 'clone_fields'):
-                        redirect_url += f"{'&' if '?' in redirect_url else '?'}{prepare_cloned_fields(obj)}"
+                        redirect_url += f"?{prepare_cloned_fields(obj)}"
 
                     return redirect(redirect_url)
 

File diff ditekan karena terlalu besar
+ 0 - 0
netbox/project-static/dist/lldp.js


File diff ditekan karena terlalu besar
+ 0 - 0
netbox/project-static/dist/lldp.js.map


File diff ditekan karena terlalu besar
+ 0 - 0
netbox/project-static/dist/netbox-dark.css


File diff ditekan karena terlalu besar
+ 0 - 0
netbox/project-static/dist/netbox-light.css


File diff ditekan karena terlalu besar
+ 0 - 0
netbox/project-static/dist/netbox-print.css


File diff ditekan karena terlalu besar
+ 0 - 0
netbox/project-static/dist/netbox.js


File diff ditekan karena terlalu besar
+ 0 - 0
netbox/project-static/dist/netbox.js.map


+ 73 - 25
netbox/project-static/src/device/lldp.ts

@@ -1,6 +1,17 @@
 import { createToast } from '../bs';
 import { getNetboxData, apiGetBase, hasError, isTruthy, toggleLoader } from '../util';
 
+// Match an interface name that begins with a capital letter and is followed by at least one other
+// alphabetic character, and ends with a forward-slash-separated numeric sequence such as 0/1/2.
+const CISCO_IOS_PATTERN = new RegExp(/^([A-Z][A-Za-z]+)[^0-9]*([0-9/]+)$/);
+
+// Mapping of overrides to default Cisco IOS interface alias behavior (default behavior is to use
+// the first two characters).
+const CISCO_IOS_OVERRIDES = new Map<string, string>([
+  // Cisco IOS abbreviates 25G (TwentyFiveGigE) interfaces as 'Twe'.
+  ['TwentyFiveGigE', 'Twe'],
+]);
+
 /**
  * Get an attribute from a row's cell.
  *
@@ -12,6 +23,40 @@ function getData(row: HTMLTableRowElement, query: string, attr: string): string
   return row.querySelector(query)?.getAttribute(attr) ?? null;
 }
 
+/**
+ * Get preconfigured alias for given interface. Primarily for matching long-form Cisco IOS
+ * interface names with short-form Cisco IOS interface names. For example, `GigabitEthernet0/1/2`
+ * would become `Gi0/1/2`.
+ *
+ * This should probably be replaced with something in the primary application (Django), such as
+ * a database field attached to given interface types. However, this is a temporary measure to
+ * replace the functionality of this one-liner:
+ *
+ * @see https://github.com/netbox-community/netbox/blob/9cc4992fad2fe04ef0211d998c517414e8871d8c/netbox/templates/dcim/device/lldp_neighbors.html#L69
+ *
+ * @param name Long-form/original interface name.
+ */
+function getInterfaceAlias(name: string | null): string | null {
+  if (name === null) {
+    return name;
+  }
+  if (name.match(CISCO_IOS_PATTERN)) {
+    // Extract the base name and numeric portions of the interface. For example, an input interface
+    // of `GigabitEthernet0/0/1` would result in an array of `['GigabitEthernet', '0/0/1']`.
+    const [base, numeric] = (name.match(CISCO_IOS_PATTERN) ?? []).slice(1, 3);
+
+    if (isTruthy(base) && isTruthy(numeric)) {
+      // Check the override map and use its value if the base name is present in the map.
+      // Otherwise, use the first two characters of the base name. For example,
+      // `GigabitEthernet0/0/1` would become `Gi0/0/1`, but `TwentyFiveGigE0/0/1` would become
+      // `Twe0/0/1`.
+      const aliasBase = CISCO_IOS_OVERRIDES.get(base) || base.slice(0, 2);
+      return `${aliasBase}${numeric}`;
+    }
+  }
+  return name;
+}
+
 /**
  * Update row styles based on LLDP neighbor data.
  */
@@ -23,38 +68,41 @@ function updateRowStyle(data: LLDPNeighborDetail) {
 
     if (row !== null) {
       for (const neighbor of neighbors) {
-        const cellDevice = row.querySelector<HTMLTableCellElement>('td.device');
-        const cellInterface = row.querySelector<HTMLTableCellElement>('td.interface');
-        const cDevice = getData(row, 'td.configured_device', 'data');
-        const cChassis = getData(row, 'td.configured_chassis', 'data-chassis');
-        const cInterface = getData(row, 'td.configured_interface', 'data');
-
-        let cInterfaceShort = null;
-        if (isTruthy(cInterface)) {
-          cInterfaceShort = cInterface.replace(/^([A-Z][a-z])[^0-9]*([0-9/]+)$/, '$1$2');
-        }
+        const deviceCell = row.querySelector<HTMLTableCellElement>('td.device');
+        const interfaceCell = row.querySelector<HTMLTableCellElement>('td.interface');
+        const configuredDevice = getData(row, 'td.configured_device', 'data');
+        const configuredChassis = getData(row, 'td.configured_chassis', 'data-chassis');
+        const configuredIface = getData(row, 'td.configured_interface', 'data');
+
+        const interfaceAlias = getInterfaceAlias(configuredIface);
 
-        const nHost = neighbor.remote_system_name ?? '';
-        const nPort = neighbor.remote_port ?? '';
-        const [nDevice] = nHost.split('.');
-        const [nInterface] = nPort.split('.');
+        const remoteName = neighbor.remote_system_name ?? '';
+        const remotePort = neighbor.remote_port ?? '';
+        const [neighborDevice] = remoteName.split('.');
+        const [neighborIface] = remotePort.split('.');
 
-        if (cellDevice !== null) {
-          cellDevice.innerText = nDevice;
+        if (deviceCell !== null) {
+          deviceCell.innerText = neighborDevice;
         }
 
-        if (cellInterface !== null) {
-          cellInterface.innerText = nInterface;
+        if (interfaceCell !== null) {
+          interfaceCell.innerText = neighborIface;
         }
 
-        if (!isTruthy(cDevice) && isTruthy(nDevice)) {
+        // Interface has an LLDP neighbor, but the neighbor is not configured in NetBox.
+        const nonConfiguredDevice = !isTruthy(configuredDevice) && isTruthy(neighborDevice);
+
+        // NetBox device or chassis matches LLDP neighbor.
+        const validNode =
+          configuredDevice === neighborDevice || configuredChassis === neighborDevice;
+
+        // NetBox configured interface matches LLDP neighbor interface.
+        const validInterface =
+          configuredIface === neighborIface || interfaceAlias === neighborIface;
+
+        if (nonConfiguredDevice) {
           row.classList.add('info');
-        } else if (
-          (cDevice === nDevice || cChassis === nDevice) &&
-          cInterfaceShort === nInterface
-        ) {
-          row.classList.add('success');
-        } else if (cDevice === nDevice || cChassis === nDevice) {
+        } else if (validNode && validInterface) {
           row.classList.add('success');
         } else {
           row.classList.add('danger');

+ 2 - 4
netbox/project-static/src/sidenav.ts

@@ -266,10 +266,8 @@ class SideNav {
       for (const link of this.getActiveLinks()) {
         this.activateLink(link, 'collapse');
       }
-      setTimeout(() => {
-        this.bodyRemove('hide');
-        this.bodyAdd('hidden');
-      }, 300);
+      this.bodyRemove('hide');
+      this.bodyAdd('hidden');
     }
   }
 

+ 7 - 1
netbox/project-static/styles/netbox.scss

@@ -197,9 +197,15 @@ table {
         text-decoration: underline;
       }
     }
+    .dropdown {
+      // Presence of 'overflow: scroll' on a table causes dropdowns to be improperly hidden when
+      // opened. See: https://github.com/twbs/bootstrap/issues/24251
+      position: static;
+    }
   }
   th {
-    a, a:hover {
+    a,
+    a:hover {
       color: $body-color;
       text-decoration: none;
     }

+ 27 - 3
netbox/project-static/styles/sidenav.scss

@@ -105,6 +105,11 @@
   // Navbar brand
   .sidenav-brand {
     margin-right: 0;
+    transition: opacity 0.1s ease-in-out;
+  }
+
+  .sidenav-brand-icon {
+    transition: opacity 0.1s ease-in-out;
   }
 
   .sidenav-inner {
@@ -141,7 +146,17 @@
   }
 
   .sidenav-toggle {
-    display: none;
+    // The sidenav toggle's default state is "hidden". Because modifying the `display` property
+    // isn't ideal for smooth transitions, combine opacity 0 (transparent) and position absolute
+    // to yield a similar result.
+    position: absolute;
+    display: inline-block;
+    opacity: 0;
+    // The transition itself is largely irrelevant, but CSS needs *something* to transition in
+    // order to apply a delay.
+    transition: opacity 10ms ease-in-out;
+    // Offset the transition delay so the icon isn't visible during the logo transition.
+    transition-delay: 0.1s;
   }
 
   .sidenav-collapse {
@@ -350,13 +365,21 @@
   .sidenav-brand {
     position: absolute;
     opacity: 0;
-    transform: translateX(-150%);
   }
 
   .sidenav-brand-icon {
     opacity: 1;
   }
 
+  .sidenav-toggle {
+    // Immediately hide the toggle when the sidenav is closed, so it doesn't linger and overlap
+    // with the logo elements.
+    opacity: 0;
+    position: absolute;
+    transition: unset;
+    transition-delay: 0ms;
+  }
+
   .navbar-nav > .nav-item {
     > .nav-link {
       &:after {
@@ -402,7 +425,8 @@
 
   @include media-breakpoint-up(lg) {
     .sidenav-toggle {
-      display: inline-block;
+      position: relative;
+      opacity: 1;
     }
   }
 }

+ 1 - 0
netbox/project-static/styles/theme-dark.scss

@@ -74,6 +74,7 @@ $btn-link-disabled-color: $gray-300;
 
 // Forms
 $component-active-bg: $primary;
+$component-active-color: $black;
 $form-text-color: $text-muted;
 $input-bg: $gray-900;
 $input-disabled-bg: $gray-700;

+ 1 - 1
netbox/templates/dcim/devicetype.html

@@ -143,7 +143,7 @@
     </div>
     <div class="row my-3">
         <div class="col col-md-12">
-            <ul class="nav nav-pills" role="tablist">
+            <ul class="nav nav-pills mb-1" role="tablist">
                 <li class="nav-item" role="presentation">
                     <button class="nav-link active" data-bs-target="#interfaces" role="tab" data-bs-toggle="tab">
                         Interfaces {% badge interface_table.rows|length %}

+ 5 - 5
netbox/templates/extras/objectchange.html

@@ -130,12 +130,12 @@
             </h5>
             <div class="card-body">
                 {% if object.postchange_data %}
-                <pre class="change-data">{% for k, v in object.postchange_data.items %}{% spaceless %}
-                    <span{% if k in diff_added %} class="added"{% endif %}>{{ k }}: {{ v|render_json }}</span>
-                    {% endspaceless %}{% endfor %}
-                </pre>
+                    <pre class="change-data">{% for k, v in object.postchange_data.items %}{% spaceless %}
+                        <span{% if k in diff_added %} class="added"{% endif %}>{{ k }}: {{ v|render_json }}</span>
+                        {% endspaceless %}{% endfor %}
+                    </pre>
                 {% else %}
-                <span class="text-muted">None</span>
+                    <span class="text-muted">None</span>
                 {% endif %}
             </div>
         </div>

+ 2 - 2
netbox/templates/extras/webhook.html

@@ -47,7 +47,7 @@
           <tr>
             <th scope="row">Update</th>
             <td>
-              {% if object.type_create %}
+              {% if object.type_update %}
                 <i class="mdi mdi-check-bold text-success" title="Yes"></i>
               {% else %}
                 <i class="mdi mdi-close-thick text-danger" title="No"></i>
@@ -57,7 +57,7 @@
           <tr>
             <th scope="row">Delete</th>
             <td>
-              {% if object.type_create %}
+              {% if object.type_delete %}
                 <i class="mdi mdi-check-bold text-success" title="Yes"></i>
               {% else %}
                 <i class="mdi mdi-close-thick text-danger" title="No"></i>

+ 12 - 9
netbox/templates/generic/object.html

@@ -6,9 +6,17 @@
 {% load plugins %}
 
 {% block header %}
-  {# Breadcrumbs #}
-  <nav class="breadcrumb-container px-3" aria-label="breadcrumb">
-    <div class="float-end">
+  <div class="d-flex justify-content-between align-items-center">
+    {# Breadcrumbs #}
+    <nav class="breadcrumb-container px-3" aria-label="breadcrumb">
+      <ol class="breadcrumb">
+        {% block breadcrumbs %}
+          <li class="breadcrumb-item"><a href="{% url object|viewname:'list' %}">{{ object|meta:'verbose_name_plural'|bettertitle }}</a></li>
+        {% endblock breadcrumbs %}
+      </ol>
+    </nav>
+    {# Object identifier #}
+    <div class="float-end px-3">
         <code class="text-muted">
           {% block object_identifier %}
             {{ object|meta:"app_label" }}.{{ object|meta:"model_name" }}:{{ object.pk }}
@@ -16,12 +24,7 @@
           {% endblock object_identifier %}
         </code>
     </div>
-    <ol class="breadcrumb">
-      {% block breadcrumbs %}
-        <li class="breadcrumb-item"><a href="{% url object|viewname:'list' %}">{{ object|meta:'verbose_name_plural'|bettertitle }}</a></li>
-      {% endblock breadcrumbs %}
-    </ol>
-  </nav>
+  </div>
   {{ block.super }}
 {% endblock %}
 

+ 35 - 33
netbox/templates/inc/table.html

@@ -1,41 +1,43 @@
 {% load django_tables2 %}
 
-<table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
+<div class="table-responsive">
+  <table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
     {% if table.show_header %}
-        <thead>
-            <tr>
-                {% for column in table.columns %}
-                    {% if column.orderable %}
-                        <th {{ column.attrs.th.as_html }}><a href="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}">{{ column.header }}</a></th>
-                    {% else %}
-                        <th {{ column.attrs.th.as_html }}>{{ column.header }}</th>
-                    {% endif %}
-                {% endfor %}
-            </tr>
-        </thead>
+      <thead>
+        <tr>
+          {% for column in table.columns %}
+            {% if column.orderable %}
+              <th {{ column.attrs.th.as_html }}><a href="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}">{{ column.header }}</a></th>
+            {% else %}
+              <th {{ column.attrs.th.as_html }}>{{ column.header }}</th>
+            {% endif %}
+          {% endfor %}
+        </tr>
+      </thead>
     {% endif %}
     <tbody>
-        {% for row in table.page.object_list|default:table.rows %}
-            <tr {{ row.attrs.as_html }}>
-                {% for column, cell in row.items %}
-                    <td {{ column.attrs.td.as_html }}>{{ cell }}</td>
-                {% endfor %}
-            </tr>
-        {% empty %}
-            {% if table.empty_text %}
-                <tr>
-                    <td colspan="{{ table.columns|length }}" class="text-center text-muted">&mdash; {{ table.empty_text }} &mdash;</td>
-                </tr>
-            {% endif %}
-        {% endfor %}
+      {% for row in table.page.object_list|default:table.rows %}
+        <tr {{ row.attrs.as_html }}>
+          {% for column, cell in row.items %}
+            <td {{ column.attrs.td.as_html }}>{{ cell }}</td>
+          {% endfor %}
+        </tr>
+      {% empty %}
+        {% if table.empty_text %}
+          <tr>
+            <td colspan="{{ table.columns|length }}" class="text-center text-muted">&mdash; {{ table.empty_text }} &mdash;</td>
+          </tr>
+        {% endif %}
+      {% endfor %}
     </tbody>
     {% if table.has_footer %}
-        <tfoot>
-            <tr>
-                {% for column in table.columns %}
-                    <td>{{ column.footer }}</td>
-                {% endfor %}
-            </tr>
-        </tfoot>
+      <tfoot>
+        <tr>
+          {% for column in table.columns %}
+            <td>{{ column.footer }}</td>
+          {% endfor %}
+        </tr>
+      </tfoot>
     {% endif %}
-</table>
+  </table>
+</div>

+ 1 - 1
netbox/utilities/templatetags/helpers.py

@@ -58,7 +58,7 @@ def render_json(value):
     """
     Render a dictionary as formatted JSON.
     """
-    return json.dumps(value, indent=4, sort_keys=True)
+    return json.dumps(value, ensure_ascii=False, indent=4, sort_keys=True)
 
 
 @register.filter()

+ 3 - 3
requirements.txt

@@ -18,11 +18,11 @@ gunicorn==20.1.0
 Jinja2==3.0.2
 Markdown==3.3.4
 markdown-include==0.6.0
-mkdocs-material==7.3.2
+mkdocs-material==7.3.4
 netaddr==0.8.0
-Pillow==8.3.2
+Pillow==8.4.0
 psycopg2-binary==2.9.1
-PyYAML==5.4.1
+PyYAML==6.0
 svgwrite==1.4.1
 tablib==3.0.0
 

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini