Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class RoutersController < DashboardController
def index
################# NEW
@routers = []
@flavors_by_id = services.networking.flavors.index_by(&:id)

if current_user.is_allowed?("context_is_cloud_network_admin")
@routers =
Expand Down Expand Up @@ -135,25 +136,42 @@ def show
device_id: @router.id,
device_owner: "network:router_interface",
)
@router_flavor = services.networking.find_flavor(@router.flavor_id) if @router.flavor_id.present?
end

def new
# build new router object (no api call done yet!)
@router = services.networking.new_router("admin_state_up" => true)
@flavors = services.networking.flavors(service_type: "L3_ROUTER_NAT")
end

def create
if params["router"]["external_gateway_info"]["network_id"].blank?
params["router"].delete("external_gateway_info")
end
# remove blank flavor_id so it's not sent to the API
flavor_id = params["router"].delete("flavor_id")
params["router"]["flavor_id"] = flavor_id if flavor_id.present?
# keep availability_zone_hints as string in model (for form re-render), convert to array before API call
az = (params["router"]["availability_zone_hints"] || "").strip
# get selected subnets and remove them from params
@selected_internal_subnets =
(params[:router].delete(:internal_subnets) || []).reject(&:empty?)
# build new router object
@router = services.networking.new_router(params[:router])
@router.internal_subnets = @selected_internal_subnets

if @router.save
# pass flavor name to model so it can validate AZ requirement for VPNaaS
if flavor_id.present?
@flavors = services.networking.flavors(service_type: "L3_ROUTER_NAT")
selected_flavor = @flavors.find { |f| f.id == flavor_id }
@router.flavor_name = selected_flavor&.name.to_s
end

if @router.valid?
# convert AZ string to array before saving to API
@router.write("availability_zone_hints", az.blank? ? [] : [az])
@router.save
# router is created -> add subnets as interfaces
services.networking.add_router_interfaces(
@router.id,
Expand All @@ -164,7 +182,7 @@ def create
flash.now[:notice] = "Router successfully created."
redirect_to plugin("networking").routers_path
else
# didn't save -> render new
@flavors ||= services.networking.flavors(service_type: "L3_ROUTER_NAT")
render action: :new
end
end
Expand Down Expand Up @@ -195,14 +213,15 @@ def edit
data["subnet_id"]
end
end
@router_flavor = services.networking.find_flavor(@router.flavor_id) if @router.flavor_id.present?
end

def update
@action_from_show =
params[:router].delete(:action_from_show) == "true" || false
# get selected subnets and remove them from params
@selected_internal_subnet_ids =
(params[:router].delete(:internal_subnets) || []).reject(&:empty?)
Array(params[:router].delete(:internal_subnets)).reject(&:empty?)

# build new router object
@router = services.networking.new_router(params[:router].to_unsafe_hash)
Expand Down
34 changes: 34 additions & 0 deletions plugins/networking/app/javascript/plugin/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,40 @@ const init = function () {
if ($("#router_external_gateway_info_network_id").val()) {
loadSubnets($("#router_external_gateway_info_network_id").val())
}

handleFlavorChange()
}

$(document).on("modal:contentUpdated", (e) => init())

const handleFlavorChange = function () {
const $flavorSelect = $("#router_flavor_id")
if ($flavorSelect.length === 0) return

const $azHint = $("#availability_zone_group .col-sm-8 > p.help-block")
const $azLabel = $("#availability_zone_group label")
const $internalSubnets = $("#router_internal_subnets").closest(".form-group")
const $immutableWarning = $("#flavor_immutable_warning")
const requiredMarker = '<abbr id="az_required_marker" title="required">*</abbr> '

const update = function () {
const isVpnaas = $flavorSelect.find("option:selected").text().toLowerCase().includes("vpnaas")
const hasFlavorSelected = $flavorSelect.val() !== ""
$azHint.toggle(isVpnaas)
$immutableWarning.toggle(hasFlavorSelected)
if (isVpnaas) {
if ($("#az_required_marker").length === 0) {
$azLabel.prepend(requiredMarker)
$azLabel.addClass("required")
}
$internalSubnets.hide()
} else {
$("#az_required_marker").remove()
$azLabel.removeClass("required")
$internalSubnets.show()
}
}

$flavorSelect.on("change", update)
update()
}
7 changes: 7 additions & 0 deletions plugins/networking/app/models/networking/flavor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

module Networking
# Represents a Neutron Network Flavor
class Flavor < Core::ServiceLayer::Model
end
end
7 changes: 6 additions & 1 deletion plugins/networking/app/models/networking/router.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ module Networking
# represents the Openstack Router
class Router < Core::ServiceLayer::Model
validates :name, presence: { message: "Please provide a name" }
validates :availability_zone_hints, presence: { message: "is required for VPNaaS routers" }, if: :vpnaas_flavor?

attr_accessor :internal_subnets
attr_accessor :internal_subnets, :flavor_name

def vpnaas_flavor?
flavor_name.to_s.downcase.include?("vpnaas")
end

def ip_subnet_objects
return @ip_subnet_objects if @ip_subnet_objects
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class NetworkingService < Core::ServiceLayer::Service
include NetworkingServices::DhcpAgent
include NetworkingServices::Asr
include NetworkingServices::BgpVpn
include NetworkingServices::Flavor

def available?(_action_name_sym = nil)
elektron.service?("network")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

module ServiceLayer
module NetworkingServices
# Implements Neutron Network Flavors API
module Flavor
def flavor_map
@flavor_map ||= class_map_proc(Networking::Flavor)
end

def flavors(filter = {})
elektron_networking.get("flavors", filter).map_to(
"body.flavors",
&flavor_map
)
rescue Elektron::Errors::ApiResponse
[]
end

def find_flavor(id)
return nil unless id
elektron_networking.get("flavors/#{id}").map_to(
"body.flavor",
&flavor_map
)
rescue Elektron::Errors::ApiResponse
nil
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@
%tr
%th Admin State
%td= @router.admin_state_up ? 'UP' : 'DOWN'
%tr
%th Network Flavor
%td
- if @router_flavor
= @router_flavor.name
%br
- if @router_flavor.description.present?
%small.text-muted= @router_flavor.description
%br
%small.text-muted= @router_flavor.id
- else
None
%tr
%th Hosting Device
%td= @router.hosting_device
Expand Down
31 changes: 24 additions & 7 deletions plugins/networking/app/views/networking/routers/edit.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
%p.alert.alert-error
= @router.errors.full_messages.to_sentence + '.'

- if @router_flavor&.name.to_s.include?("vpnaas")
%p.alert.alert-info
VPNaaS routers cannot have internal networks attached. Use BGPVPN for connectivity.

= f.input :action_from_show, :as => :hidden, :input_html => { :value => @action_from_show }

= f.input :name
Expand All @@ -25,13 +29,26 @@
collection: @subnets,
selected: @router_external_subnet_ids

= f.input :internal_subnets,
label: "Private Network Subnets",
wrapper: :horizontal_radio_and_checkboxes_4x8_scrollable,
required: true,
as: :check_boxes,
collection: @internal_subnets.sort{|a,b| a.network_name<=>b.network_name}.map{|n| ["#{n.name} (#{n.network_name})",n.id]},
checked: @router_internal_subnet_ids
- if @router_flavor&.name.to_s.include?("vpnaas")
.form-group
%label.col-sm-4.control-label Network Flavor
.col-sm-8
%p.form-control-static
= @router_flavor.name
- if @router_flavor.description.present?
\–
= @router_flavor.description
%br
%small.text-muted= @router_flavor.id
= f.input :internal_subnets, as: :hidden
- else
= f.input :internal_subnets,
label: "Private Network Subnets",
wrapper: :horizontal_radio_and_checkboxes_4x8_scrollable,
required: true,
as: :check_boxes,
collection: @internal_subnets.sort{|a,b| a.network_name<=>b.network_name}.map{|n| ["#{n.name} (#{n.network_name})",n.id]},
checked: @router_internal_subnet_ids



Expand Down
16 changes: 14 additions & 2 deletions plugins/networking/app/views/networking/routers/index.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
%th External Network
%th External Subnet
%th Private Network
%th Flavor
%th Status
%th.snug
%tbody
Expand Down Expand Up @@ -49,6 +50,17 @@
%br
%span.info-text= net.id

%td= router.status
%td
- flavor = @flavors_by_id[router.flavor_id]
- if flavor
%span.label.label-default{title: flavor.description}= flavor.name
%td

- status_class = case router.status
- when 'ACTIVE' then 'label-success'
- when 'ERROR' then 'label-danger'
- when 'DOWN' then 'label-default'
- else 'label-warning'
%span.label{class: status_class}= router.status
%td.snug
= render partial: 'item_actions', locals: {router:router, show_view:false}
= render partial: 'item_actions', locals: {router:router, show_view:false}
15 changes: 15 additions & 0 deletions plugins/networking/app/views/networking/routers/new.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,26 @@
%p.alert.alert-error
= @router.errors.full_messages.to_sentence + '.'

%p.alert.alert-warning#flavor_immutable_warning{style: 'display:none'}
The selected flavor cannot be changed after the router is created.

= f.input :name
= f.input :admin_state_up, {label: 'Admin State',
as: :select,
collection: [['UP', 'true'],['DOWN', 'false']]}

- if @flavors.present?
- flavor_options = @flavors.select(&:enabled).map { |fl| [fl.description.present? ? "#{fl.name} (#{fl.description})" : fl.name, fl.id] }
= f.input :flavor_id, label: 'Network Flavor',
as: :select,
include_blank: 'None (standard router)',
collection: flavor_options,
required: false,
input_html: { id: 'router_flavor_id' },
icon_hint: "The flavor cannot be changed after the router is created."

= f.input :availability_zone_hints, label: 'Availability Zone', as: :string, required: false, placeholder: 'e.g. qa-de-1a', input_html: { id: 'router_availability_zone_hints' }, icon_hint: "VPNaaS routers require an availability zone. For redundancy, consider creating a second VPN router in a different availability zone.", wrapper_html: { id: 'availability_zone_group' }


= f.simple_fields_for :external_gateway_info do |info|
= info.input :network_id, {label: "Floating IP Network",
Expand Down
Loading