Page Menu
Home
GRNET
Search
Configure Global Search
Log In
Files
F1614919
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Sat, Mar 21, 11:24 AM
Size
26 KB
Mime Type
text/x-diff
Expires
Mon, Mar 23, 11:24 AM (23 h, 2 m)
Engine
blob
Format
Raw Data
Handle
353011
Attached To
rWEBDNS WebDNS (edet4)
View Options
diff --git a/app/assets/javascripts/bulky.coffee b/app/assets/javascripts/bulky.coffee
index a3cd8df..ccc7e76 100644
--- a/app/assets/javascripts/bulky.coffee
+++ b/app/assets/javascripts/bulky.coffee
@@ -1,177 +1,206 @@
class window.Bulky
constructor: (valid_record_url, submit_url) ->
@enabled = false
@deletes = {}
@changes = {}
@additions = {}
@add_counter = 0
@valid_record_url = valid_record_url
@submit_url = submit_url
@hooked = false
@hook()
panel_update: () ->
added = (a for own a of @additions).length
changed = (c for own c of @changes).length
deleted = (d for own d of @deletes).length
$('#bulk-panel .added').text(added)
$('#bulk-panel .changed').text(changed)
$('#bulk-panel .deleted').text(deleted)
marked_for_delete: (id) ->
return @deletes[id]
update: (id, attr, value) ->
@changes[id] or= {}
@changes[id][attr] = value
$("#record-#{id}").addClass('modified')
@panel_update()
add: (rec) ->
@add_counter += 1
@additions[@add_counter] = rec
@panel_update()
@add_counter
validate_add: (arr) ->
_me = this
$.ajax {
url: @valid_record_url,
type: 'post',
dataType: 'json',
data: arr,
success: (data) ->
if data.errors
alert(data.errors)
return
id = _me.add(data.record)
_me.render_new(id, data.record)
error: () ->
alert('There was an error processing your request...')
}
render_new: (id, rec) ->
el = $('#new_records .clone').clone()
el.removeClass('clone').removeClass('hidden')
el.attr('id', "fresh-#{id}")
for attr in ['name', 'ttl', 'type', 'prio', 'content']
if rec[attr]
el.find(".#{attr}").text(rec[attr])
el.find('a.js-destroy').data('id', id).data('fresh', true)
$('#new_records tbody').append(el)
$('#new_records').removeClass('hidden')
+ render_errors: (errors) ->
+ if errors.deletes
+ deleted = ("#record-#{d}" for own d of errors.deletes)
+ txt = " (#{deleted.length} failed to delete)"
+ $('#bulk-panel .failed-deleted').text(txt).data(ids:deleted)
+ if errors.changes
+ changed = ("#record-#{c}" for own c of errors.changes)
+ txt = " (#{changed.length} failed to be updated)"
+ $('#bulk-panel .failed-changed').text(txt).data(ids:changed)
+ if errors.additions
+ added = ("#fresh-#{a}" for own a of errors.additions)
+ txt = " (#{added.length} failed to be added)"
+ $('#bulk-panel .failed-added').text(txt).data(ids:added)
+
+ render_clear_errors: ->
+ $('#bulk-panel .failed').text('')
+
enable: ->
return if @enabled
$('#bulk-panel').removeClass('hidden')
$('#new_record .btn').attr('value', 'Bulk Add')
$('#records .js-bulk-hide').hide()
@enabled = true
disable: ->
return if !@enabled
$('#bulk-panel').addClass('hidden')
$('#inline-record-form .btn').attr('value', 'Add')
$('#records .js-bulk-hide').show()
@enabled = false
commit: ->
data = {
deletes: id for id, _ of @deletes,
- changes: rec for id, rec of @changes when !@deletes[id] ,
+ # changes: rec for id, rec of @changes when !@deletes[id],
+ changes: @changes,
additions: @additions
}
+ @render_clear_errors()
_me = this
$.ajax {
url: @submit_url,
type: 'post',
data: JSON.stringify(data),
dataType: 'json',
contentType:'application/json',
success: (data) ->
console.log data
+ if data.errors
+ _me.render_errors(data.errors)
+ return
+ alert('Bulk operations successfully committed!')
+ location.reload()
error: () ->
alert('There was an error submiting bulk operations')
}
hook: ->
return if @hooked
_me = this
# Hook bulky buttons
$('#js-bulky-activate').click ->
_me.enable()
$(this).parents('li').remove()
$('#js-bulky-cancel').click ->
_me.disable()
$('#js-bulky-commit').click ->
_me.commit()
$('#bulk-panel .js-modified-hover').hover \
-> $('.modified').addClass('highlight')
,
-> $('.modified').removeClass('highlight')
+
+ $('#bulk-panel .failed').hover \
+ -> $($(this).data('ids').join(', ')).addClass('highlight')
+ ,
+ -> $($(this).data('ids').join(', ')).removeClass('highlight')
# Hook destroy button
$('#records, #new_records').on 'click', 'a.js-destroy', () ->
return true if !_me.enabled
link = $(this)
id = link.data('id')
fresh = link.data('fresh')
# Drop a newly created record
if fresh
delete _me.additions[id]
link.parents("#fresh-#{id}").remove()
# Resurrect a delete record
else if link.data('deleted')
delete _me.deletes[id]
link.data('deleted', false)
link.parents('tr').removeClass('danger')
link.parents('tr').attr('title', 'Remove')
link.find('span').removeClass('glyphicon-plus').addClass('glyphicon-remove')
# Delete a record
else
_me.deletes[id] = true
link.data('deleted', true)
link.parents('tr').addClass('danger')
link.find('abbr').attr('title', 'Undo')
link.find('span').removeClass('glyphicon-remove').addClass('glyphicon-plus')
_me.panel_update()
return false;
# Hook add button
$('#new_record').submit () ->
return true if !_me.enabled
_me.validate_add $(this).serializeArray()
return false;
@hooked = true
debug: ->
console.log("Bulky, enabled=#{@enabled}")
for id, rec of @additions
change = for k, v of rec
"#{k}: #{v}"
console.log("Add #{id}: #{change}")
for id, rec of @changes
change = for k, v of rec
"#{k}: #{v}"
console.log("Change #{id}: #{change}")
for id, _ of @deletes
console.log("Delete #{id}")
diff --git a/app/controllers/records_controller.rb b/app/controllers/records_controller.rb
index 1a3e056..3e8c091 100644
--- a/app/controllers/records_controller.rb
+++ b/app/controllers/records_controller.rb
@@ -1,128 +1,133 @@
class RecordsController < ApplicationController
before_action :authenticate_user!
before_action :domain, except: [:search]
before_action :editable_transform_params, only: [:editable]
before_action :record, only: [:edit, :update, :editable, :destroy]
# GET /records/new
def new
@record = domain.records.build
end
# GET /records/1/edit
def edit
end
# POST /records
def create
@record = domain.records.new(new_record_params)
if @record.save
notify_record(@record, :create)
redirect_to domain, notice: 'Record was successfully created.'
else
flash[:alert] = 'There were some errors creating the record!'
render :new
end
end
# PATCH/PUT /records/1
def update
if @record.update(edit_record_params)
notify_record(@record, :update)
redirect_to domain, notice: 'Record was successfully updated.'
else
render :edit
end
end
def valid
@record = domain.records.new(new_record_params)
if @record.valid?
response = {
record: @record.as_bulky_json,
errors: false
}
render json: response
else
render json: { errors: @record.errors.full_messages.join(', ') }
end
end
def bulk
- render json: { ok: true }
+ err = @domain.bulk(params)
+ if err.empty?
+ render json: { ok: true }
+ else
+ render json: { errors: err }
+ end
end
def editable
@record.assign_attributes(edit_record_params)
if @record.valid?
if @save
@record.save!
notify_record(@record, :update)
end
response = {
attribute: @attr,
value: @record.read_attribute(@attr),
serial: @domain.soa(true).serial,
record: @record.as_bulky_json,
saved: @save
}
render json: response
else
render text: @record.errors[@attr].join(', '), status: 400
end
end
# DELETE /records/1
def destroy
@record.destroy
notify_record(@record, :destroy)
redirect_to domain, notice: 'Record was successfully destroyed.'
end
# GET /search
def search
@records = Record
.where(domain: show_domain_scope)
.includes(:domain)
.search(params[:q]) # scope by domain
@records = Record.smart_order(@records)
end
private
# Modify params to use standard Rails patterns
def editable_transform_params
@attr = params[:name]
@save = params[:save] != 'false'
params[:record] = { params[:name] => params[:value] }
end
def edit_record_params
if @record.type == 'SOA'
permitted = [:contact, :serial, :refresh, :retry, :expire, :nx]
else
permitted = [:name, :content, :ttl, :prio, :disabled]
end
params.require(:record).permit(*permitted).tap { |r|
r[:drop_privileges] = true if not admin?
}
end
def new_record_params
params.require(:record).permit(:name, :content, :ttl, :type, :prio).tap { |r|
r[:drop_privileges] = true if not admin?
}
end
def notify_record(*args)
notification.notify_record(current_user, *args) if WebDNS.settings[:notifications]
end
end
diff --git a/app/models/domain.rb b/app/models/domain.rb
index ac10ef8..4ad9dd7 100644
--- a/app/models/domain.rb
+++ b/app/models/domain.rb
@@ -1,230 +1,277 @@
class Domain < ActiveRecord::Base
class NotAChild < StandardError; end
self.inheritance_column = :nx
# List all supported domain types.
def self.domain_types
[
'NATIVE',
'MASTER',
'SLAVE',
]
end
# List domain types that can be created.
def self.allowed_domain_types
domain_types - WebDNS.settings[:prohibit_domain_types]
end
# List parent authorities
def self.dnssec_parent_authorities
WebDNS.settings[:dnssec_parent_authorities]
end
# Fire event after transaction commmit
# Changing state inside a hook messes things up,
# this trick handles that
attr_accessor :fire_event
belongs_to :group
has_many :jobs
has_many :records
# BUG in bump_serial_trigger
has_one :soa, -> { unscope(where: :type).where(type: 'soa') }, class_name: SOA
validates :group_id, presence: true
validates :name, uniqueness: true, presence: true
validates :type, presence: true, inclusion: { in: domain_types }
validates :master, presence: true, ipv4: true, if: :slave?
validates :dnssec, inclusion: { in: [false] }, unless: :dnssec_elegible?
validates :dnssec_parent_authority, inclusion: { in: dnssec_parent_authorities }, if: :dnssec?
validates :dnssec_parent, hostname: true, if: :dnssec?
after_create :generate_soa
after_create :generate_ns
after_create :install
before_save :check_convert
after_commit :after_commit_event
attr_writer :serial_strategy
def self.dnssec_progress(current_state)
progress = [
:pending_signing, # 1/3
:wait_for_ready, # 2/3
:pending_ds] # 3/3
idx = progress.index(current_state.to_sym)
return if idx.nil?
[idx+1, progress.size].join('/')
end
state_machine initial: :initial do
after_transition(any => :pending_install) { |domain, _t| Job.add_domain(domain) }
after_transition(any => :pending_remove) { |domain, _t| Job.shutdown_domain(domain) }
after_transition(any => :pending_signing) { |domain, _t| Job.dnssec_sign(domain) }
after_transition(any => :wait_for_ready) { |domain, _t| Job.wait_for_ready(domain) }
after_transition(any => :pending_ds) { |domain, t| Job.dnssec_push_ds(domain, *t.args) }
after_transition(any => :pending_plain) { |domain, _t| Job.convert_to_plain(domain) }
after_transition(any => :destroy) { |domain, _t| domain.destroy }
# User events
event :install do
transition initial: :pending_install
end
event :dnssec_sign do
transition operational: :pending_signing
end
event :signed do
transition pending_signing: :wait_for_ready
end
event :push_ds do
# TODO: push_ds is triggered on multiple occasions
# operational: :operational
transition wait_for_ready: :pending_ds
end
event :plain_convert do
transition operational: :pending_plain
end
event :remove do
transition operational: :pending_remove
end
# Machine events
event :installed do
transition pending_install: :operational
end
event :converted do
transition [:pending_ds, :pending_plain] => :operational
end
event :cleaned_up do
transition pending_remove: :destroy
end
end
# Returns true if this domain is elegigble for DNSSEC
def dnssec_elegible?
return false if slave?
true
end
# Get the zone's serial strategy.
#
# Returns one of the supported serial strategies.
def serial_strategy
@serial_strategy ||= WebDNS.settings[:serial_strategy]
end
# Returns true if this a reverse zone.
def reverse?
name.end_with?('.in-addr.arpa') || name.end_with?('.ip6.arpa')
end
# Returns true if this a ENUM zone.
def enum?
name.end_with?('.e164.arpa')
end
# Returns true if this is a slave zone.
def slave?
type == 'SLAVE'
end
# Compute subnet for reverse records
def subnet
return if not reverse?
if name.end_with?('.in-addr.arpa')
subnet_v4
elsif name.end_with?('.ip6.arpa')
subnet_v6
end
end
def self.replace_ds(parent, child, records)
parent = find_by_name!(parent)
fail NotAChild if not child.end_with?(parent.name)
existing = parent.records.where(name: child, type: 'DS')
recs = records.map { |rec| DS.new(domain: parent, name: child, content: rec) }
ActiveRecord::Base.transaction do
existing.destroy_all
recs.map(&:save!)
end
end
+ # Apply bulk to operations to the zones
+ #
+ # 1) Deletions
+ # 2) Changes
+ # 3) Additions
+ def bulk(opts)
+ deletes = opts[:deletes] || []
+ changes = opts[:changes] || {}
+ additions = opts[:additions] || {}
+ errors = Hash.new { |h, k| h[k] = {} }
+
+ ActiveRecord::Base.transaction do
+ # Deletes
+ to_delete = records.where(id: deletes).index_by(&:id)
+ deletes.each { |rec_id|
+ if rec = to_delete[Integer(rec_id)]
+ rec.destroy
+ next
+ end
+
+ errors[:deletes][rec_id] = 'Deleted record not found'
+ }
+
+ # Changes
+ to_change = records.where(id: changes.keys).index_by(&:id)
+ changes.each {|rec_id, changes|
+ binding
+ if rec = to_change[Integer(rec_id)]
+ errors[:changes][rec_id] = rec.errors.full_messages.join(', ') if !rec.update(changes)
+ next
+ end
+
+ errors[:changes][rec_id] = 'Changed record not found'
+ }
+
+ # Additions
+ additions.each { |inc, attrs|
+ rec = records.new(attrs)
+ errors[:additions][inc] = rec.errors.full_messages.join(', ') if !rec.save
+ }
+
+ raise ActiveRecord::Rollback if errors.any?
+ end
+
+ errors
+ end
+
private
def subnet_v4
# get ip octets (remove .in-addr.arpa)
octets = name.split('.')[0...-2].reverse
return if octets.any? { |_| false }
mask = 8 * octets.size
octets += [0, 0, 0, 0]
ip = IPAddr.new octets[0, 4].join('.')
[ip, mask].join('/')
end
def subnet_v6
nibbles = name.split('.')[0...-2].reverse
return if nibbles.any? { |_| false }
mask = 4 * nibbles.size
nibbles += [0] * 32
ip = IPAddr.new nibbles[0, 32].in_groups_of(4).map(&:join).join(':')
[ip, mask].join('/')
end
# Hooks
def generate_soa
soa_record = SOA.new(domain: self)
soa_record.save!
end
def generate_ns
return if slave?
return if WebDNS.settings[:default_ns].empty?
WebDNS.settings[:default_ns].each { |ns|
Record.find_or_create_by!(domain: self, type: 'NS', name: '', content: ns)
}
end
def check_convert
return if !dnssec_changed?
event = dnssec ? :dnssec_sign : :plain_convert
if state_events.include?(event)
self.fire_event = event # Schedule event for after commit
return true
end
errors.add(:dnssec, 'You cannot modify dnssec settings in this state!')
false
end
def after_commit_event
return if !fire_event
fire_state_event(fire_event)
self.fire_event = nil
end
end
diff --git a/app/views/domains/_bulk_panel.html.erb b/app/views/domains/_bulk_panel.html.erb
index a465234..cfd7b35 100644
--- a/app/views/domains/_bulk_panel.html.erb
+++ b/app/views/domains/_bulk_panel.html.erb
@@ -1,16 +1,18 @@
<div id="bulk-panel" class="panel panel-primary hidden">
<div class="panel-heading">
Bulk Mode
</div>
<div class="panel-body">
<p>
- <span class="added">0</span> records added,
- <abbr class="js-modified-hover" title="Highlight modified records"><span class="changed">0</span> records modified</abbr>,
- <span class="deleted">0</span> records deleted.
+ <span class="added">0</span> records added<abbr title="Highlight records that failed to be added" class="failed failed-added"></abbr>,
+ <abbr class="js-modified-hover" title="Highlight modified records">
+ <span class="changed">0</span> records modified
+ </abbr><abbr title="Highlight records that failed to update." class="failed failed-changed"></abbr>,
+ <span class="deleted">0</span> records deleted<abbr title="Highlight records that failed to delete" class="failed failed-deleted"></abbr>.
</p>
<div>
<a href="." class="btn btn-default">Cancel (Refresh)</a>
<button type="button" class="btn btn-success" id="js-bulky-commit">Commit</button>
</div>
</div>
</div>
diff --git a/test/models/domain_test.rb b/test/models/domain_test.rb
index 0e2cf0d..ef79d41 100644
--- a/test/models/domain_test.rb
+++ b/test/models/domain_test.rb
@@ -1,169 +1,220 @@
require 'test_helper'
class DomainTest < ActiveSupport::TestCase
def setup
@domain = build(:domain)
end
test 'automatic SOA creation' do
@domain.save!
@domain.reload
assert_not_nil @domain.soa
end
test 'increment serial on new record' do
@domain.save!
soa = @domain.soa
assert_serial_update soa do
www = A.new(name: 'www', domain: @domain, content: '1.2.3.4')
www.save!
end
end
test 'increment serial on record update' do
@domain.save!
www = A.new(name: 'www', domain: @domain, content: '1.2.3.4')
www.save!
soa = @domain.soa.reload
assert_serial_update soa do
www.content = '1.2.3.5'
www.save!
end
end
test 'automatic NS creation' do
@domain.save!
@domain.reload
assert_equal WebDNS.settings[:default_ns].sort,
@domain.records.where(type: 'NS').pluck(:content).sort
end
test 'increment serial on record destroy' do
@domain.save!
www = A.new(name: 'www', domain: @domain, content: '1.2.3.4')
www.save!
soa = @domain.soa.reload
assert_serial_update soa do
www.destroy!
end
end
class SlaveDomainTest < ActiveSupport::TestCase
def setup
@domain = build(:slave)
end
test 'saves' do
@domain.save
assert_empty @domain.errors
end
test 'automatic SOA creation' do
@domain.save!
@domain.reload
assert_not_nil @domain.soa
assert_equal 1, @domain.soa.serial
end
test 'validates master' do
@domain.master = 'not-an-ip'
@domain.save
assert_not_empty @domain.errors['master']
end
test 'no records are allowed for users' do
@domain.save!
rec = build(:a, domain_id: @domain.id)
assert_not rec.valid?
assert_not_empty rec.errors[:type]
end
end
class StatesDomainTest < ActiveSupport::TestCase
def setup
@domain = build(:domain)
end
test 'domain lifetime' do
assert_equal 'initial', @domain.state
# Create
assert_jobs do
@domain.save! # user triggered
assert_equal 'pending_install', @domain.state
end
@domain.installed # job triggered
assert_equal 'operational', @domain.state
# Convert to dnssec (sign)
assert_jobs do
assert @domain.dnssec_sign # user triggered
assert_equal 'pending_signing', @domain.state
end
assert_jobs do
assert @domain.signed # job triggered
assert_equal 'wait_for_ready', @domain.state
end
# Convert to dnssec (publish ds)
assert_jobs do
assert @domain.push_ds([:dss1, :dss2]) # DS script triggered
assert_equal 'pending_ds', @domain.state
end
assert @domain.converted # job triggered
assert_equal 'operational', @domain.state
# Convert to plain
assert_jobs do
assert @domain.plain_convert # user triggered
assert_equal 'pending_plain', @domain.state
end
assert @domain.converted # job triggered
assert_equal 'operational', @domain.state
# Remove
assert_jobs do
assert @domain.remove # user triggered
assert_equal 'pending_remove', @domain.state
end
assert @domain.cleaned_up # job triggered
assert_equal 'destroy', @domain.state
end
end
class DsDomainTest < ActiveSupport::TestCase
def setup
@domain = create(:domain)
@ds = [
'31406 8 1 189968811e6eba862dd6c209f75623d8d9ed9142',
'31406 8 2 f78cf3344f72137235098ecbbd08947c2c9001c7f6a085a17f518b5d8f6b916d',
]
@child = "dnssec.#{@domain.name}"
@extra = DS.create(domain: @domain, name: @child, content: 'other')
end
test 'add ds records' do
Domain.replace_ds(@domain.name, @child, @ds)
@extra.save! # Should be deleted
assert_equal @ds.size, DS.where(name: "dnssec.#{@domain.name}").count
@ds.each { |ds|
assert_equal 1, DS.where(name: "dnssec.#{@domain.name}", content: ds).count
}
end
test 'check if child is a valid subdomain' do
assert_raise Domain::NotAChild do
Domain.replace_ds(@domain.name, 'dnssec.example.net', @ds)
end
end
end
+
+ class BulkTest < ActiveSupport::TestCase
+ def setup
+ @domain = create(:domain)
+ @a = create(:a, domain: @domain)
+ @aaaa = create(:aaaa, domain: @domain)
+ @new = build(:mx, domain: @domain)
+
+ end
+
+ def valid_changes
+ @valid_changes ||= begin
+ {}.tap { |c|
+ c[:deletes] = [@a.id]
+ c[:changes] = { @aaaa.id => { content: '::42' }}
+ c[:additions] = { 1 => @new.as_bulky_json }
+ }
+ end
+ end
+
+ def invalid_changes
+ @invalid_changes ||= begin
+ {}.tap { |c|
+ c[:deletes] = [Record.maximum(:id) + 1]
+ c[:changes] = { @aaaa.id => { content: '1.2.3.4' }}
+ c[:additions] = { 1 => @new.as_bulky_json.update(prio: -1) }
+ }
+ end
+ end
+
+ test 'apply changes not' do
+ err = @domain.bulk invalid_changes
+
+ assert_not_empty err
+ assert_includes err[:deletes][Record.maximum(:id) + 1], 'record not found'
+ assert_includes err[:changes][@aaaa.id], 'not a valid IPv6'
+ assert_includes err[:additions][1], 'not a valid DNS priority'
+ end
+
+ test 'apply changes' do
+ err = @domain.bulk valid_changes
+
+ @domain.reload
+ @aaaa.reload
+
+ assert_empty err
+ assert_empty @domain.records.where(id: @a.id)
+ assert_equal '::42', @aaaa.content
+ assert_equal 1, @domain.records.where(type: :mx).count
+ end
+ end
end
Event Timeline
Log In to Comment