Commit 729c223f authored by Justin Pomeroy's avatar Justin Pomeroy

Add monitor tab to create load balancer workflow

This adds the monitor tab to the create load balancer workflow and
allows adding a monitor to the pool.

Partially-Implements: blueprint horizon-lbaas-v2-ui
Change-Id: I23ee916f283b2b3f782327f16ca1624a98568cb0
parent 9c4ca8d5
......@@ -105,6 +105,10 @@ def create_pool(request, **kwargs):
kwargs = {'callback_kwargs': {'pool_id': pool['id'],
'index': 0}}
thread.start_new_thread(poll_loadbalancer_status, args, kwargs)
elif data.get('monitor'):
args = (request, kwargs['loadbalancer_id'], add_monitor)
kwargs = {'callback_kwargs': {'pool_id': pool['id']}}
thread.start_new_thread(poll_loadbalancer_status, args, kwargs)
return pool
......@@ -133,10 +137,36 @@ def add_member(request, **kwargs):
kwargs = {'callback_kwargs': {'pool_id': kwargs['pool_id'],
'index': index}}
thread.start_new_thread(poll_loadbalancer_status, args, kwargs)
elif data.get('monitor'):
args = (request, kwargs['loadbalancer_id'], add_monitor)
kwargs = {'callback_kwargs': {'pool_id': kwargs['pool_id']}}
thread.start_new_thread(poll_loadbalancer_status, args, kwargs)
return member
def add_monitor(request, **kwargs):
"""Create a new health monitor for a pool.
"""
data = request.DATA
monitorSpec = {
'type': data['monitor']['type'],
'delay': data['monitor']['interval'],
'timeout': data['monitor']['timeout'],
'max_retries': data['monitor']['retry'],
'pool_id': kwargs['pool_id']
}
if data['monitor'].get('method'):
monitorSpec['http_method'] = data['monitor']['method']
if data['monitor'].get('path'):
monitorSpec['url_path'] = data['monitor']['path']
if data['monitor'].get('status'):
monitorSpec['expected_codes'] = data['monitor']['status']
return neutronclient(request).create_lbaas_healthmonitor(
{'healthmonitor': monitorSpec}).get('healthmonitor')
@urls.register
class LoadBalancers(generic.View):
"""API for load balancers.
......
......@@ -51,7 +51,9 @@ ADD_JS_FILES = [
('dashboard/project/lbaasv2/loadbalancers/actions/create/pool/'
'pool.controller.js'),
('dashboard/project/lbaasv2/loadbalancers/actions/create/members/'
'members.controller.js')
'members.controller.js'),
('dashboard/project/lbaasv2/loadbalancers/actions/create/monitor/'
'monitor.controller.js')
]
ADD_JS_SPEC_FILES = [
......@@ -78,7 +80,9 @@ ADD_JS_SPEC_FILES = [
('dashboard/project/lbaasv2/loadbalancers/actions/create/pool/'
'pool.controller.spec.js'),
('dashboard/project/lbaasv2/loadbalancers/actions/create/members/'
'members.controller.spec.js')
'members.controller.spec.js'),
('dashboard/project/lbaasv2/loadbalancers/actions/create/monitor/'
'monitor.controller.spec.js')
]
ADD_SCSS_FILES = [
......
......@@ -29,12 +29,16 @@
'horizon.dashboard.project.lbaasv2.loadbalancers'
])
.config(config)
/* eslint-disable max-len */
.constant('horizon.dashboard.project.lbaasv2.patterns', {
/* eslint-disable max-len */
ipv4: '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$',
ipv6: '^((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?$'
ipv6: '^((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?$',
/* eslint-enable max-len */
// HTTP status codes - comma separated numbers or ranges
httpStatusCodes: /^\d+(\s*-\s*\d+)?(,\s*\d+(\s*-\s*\d+)?)*$/,
// URL path - must start with "/" and can include anything after that
urlPath: /^((\/)|(\/[^/]+)+)$/
})
/* eslint-enable max-len */
.constant('horizon.dashboard.project.lbaasv2.popovers', {
ipAddresses: '<ul><li ng-repeat="addr in member.addresses">{$ addr.ip $}</li></ul>'
});
......@@ -42,13 +46,10 @@
config.$inject = [
'$provide',
'$windowProvider',
'$routeProvider',
'$locationProvider'
'$routeProvider'
];
function config($provide, $windowProvider, $routeProvider, $locationProvider) {
$locationProvider.html5Mode(true).hashPrefix('!');
function config($provide, $windowProvider, $routeProvider) {
var href = '/project/ngloadbalancersv2/';
var basePath = $windowProvider.$get().STATIC_URL + 'dashboard/project/lbaasv2/';
$provide.constant('horizon.dashboard.project.lbaasv2.basePath', basePath);
......
......@@ -41,19 +41,52 @@
});
});
describe('LBaaS v2 Module Constants', function () {
var patterns, popovers;
beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(inject(function ($injector) {
patterns = $injector.get('horizon.dashboard.project.lbaasv2.patterns');
popovers = $injector.get('horizon.dashboard.project.lbaasv2.popovers');
}));
it('should define patterns', function () {
expect(patterns).toBeDefined();
});
it('should define expected patterns', function () {
expect(Object.keys(patterns).length).toBe(4);
var keys = ['ipv4', 'ipv6', 'httpStatusCodes', 'urlPath'];
angular.forEach(keys, function(key) {
expect(patterns[key]).toBeDefined();
});
});
it('should define popovers', function () {
expect(popovers).toBeDefined();
});
it('should define expected popover templates', function () {
expect(Object.keys(popovers).length).toBe(1);
var keys = ['ipAddresses'];
angular.forEach(keys, function(key) {
expect(popovers[key]).toBeDefined();
});
});
});
describe('LBaaS v2 Module Config', function () {
var $routeProvider, $locationProvider, basePath;
var $routeProvider, basePath;
beforeEach(function() {
// Create a dummy module so that we can test $routeProvider and $locationProvider calls
// in our actual config block.
// Create a dummy module so that we can test $routeProvider calls in our actual
// config block.
angular.module('configTest', [])
.config(function(_$routeProvider_, _$locationProvider_, $windowProvider) {
.config(function(_$routeProvider_, $windowProvider) {
$routeProvider = _$routeProvider_;
$locationProvider = _$locationProvider_;
basePath = $windowProvider.$get().STATIC_URL + 'dashboard/project/lbaasv2/';
spyOn($routeProvider, 'when').and.callThrough();
spyOn($locationProvider, 'html5Mode').and.callThrough();
});
module('ngRoute');
module('configTest');
......@@ -61,10 +94,6 @@
inject();
});
it('should use html5 mode', function () {
expect($locationProvider.html5Mode).toHaveBeenCalledWith(true);
});
it('should route URLs', function () {
var href = '/project/ngloadbalancersv2/';
var routes = [
......
......@@ -33,7 +33,7 @@
</thead>
<tbody>
<tr ng-if="ctrl.tableData.allocated.length === 0">
<td colspan="0">
<td colspan="100">
<div class="no-rows-help">
{$ ::trCtrl.helpText.noneAllocText $}
</div>
......@@ -93,7 +93,7 @@
</td>
</tr>
<tr ng-repeat-end class="detail-row">
<td class="detail" colspan="0">
<td class="detail" colspan="100">
<div class="row">
<dl class="rsp-alt-p2 col-sm-2">
<dt translate>Name</dt>
......@@ -117,7 +117,7 @@
hz-table class="table-striped table-rsp table-detail modern">
<thead>
<tr>
<th class="search-header" colspan="0">
<th class="search-header" colspan="100">
<hz-search-bar group-classes="input-group-sm" icon-classes="fa-search">
</hz-search-bar>
</th>
......@@ -132,7 +132,7 @@
</thead>
<tbody>
<tr ng-if="trCtrl.numAvailable() === 0">
<td colspan="0">
<td colspan="100">
<div class="no-rows-help">
{$ ::trCtrl.helpText.noneAvailText $}
</div>
......@@ -168,7 +168,7 @@
</td>
</tr>
<tr ng-repeat-end class="detail-row">
<td class="detail" colspan="0">
<td class="detail" colspan="100">
<div class="row">
<dl class="rsp-alt-p2 col-sm-2">
<dt translate>Name</dt>
......
......@@ -79,6 +79,8 @@
listenerProtocols: ['TCP', 'HTTP', 'HTTPS'],
poolProtocols: ['TCP', 'HTTP', 'HTTPS'],
methods: ['ROUND_ROBIN', 'LEAST_CONNECTIONS', 'SOURCE_IP'],
monitorTypes: ['HTTP', 'HTTPS', 'PING', 'TCP'],
monitorMethods: ['GET', 'HEAD'],
/**
* api methods for UI controllers
......@@ -119,6 +121,15 @@
protocol: null,
method: null
},
monitor: {
type: null,
interval: null,
retry: null,
timeout: null,
method: 'GET',
status: '200',
path: '/'
},
members: []
};
......@@ -175,6 +186,17 @@
delete finalSpec.pool;
}
cleanFinalSpecMembers(finalSpec);
cleanFinalSpecMonitor(finalSpec);
removeNulls(finalSpec);
finalSpec.loadbalancer.subnet = finalSpec.loadbalancer.subnet.id;
return lbaasv2API.createLoadBalancer(finalSpec);
}
function cleanFinalSpecMembers(finalSpec) {
// Members require a pool, address, subnet, and port but the wizard requires the address,
// subnet, and port so we can assume those exist here.
if (!finalSpec.pool || finalSpec.members.length === 0) {
......@@ -189,8 +211,27 @@
member.subnet = member.address.subnet;
member.address = member.address.ip;
});
}
function cleanFinalSpecMonitor(finalSpec) {
// Delete null properties
// Monitor requires a pool, interval, retry count, and timeout
if (!finalSpec.pool ||
!angular.isNumber(finalSpec.monitor.interval) ||
!angular.isNumber(finalSpec.monitor.retry) ||
!angular.isNumber(finalSpec.monitor.timeout)) {
delete finalSpec.monitor;
}
// Only include necessary monitor properties
if (finalSpec.monitor && finalSpec.monitor.type !== 'HTTP') {
delete finalSpec.monitor.method;
delete finalSpec.monitor.status;
delete finalSpec.monitor.path;
}
}
function removeNulls(finalSpec) {
angular.forEach(finalSpec, function deleteNullsForGroup(group, groupName) {
angular.forEach(group, function deleteNullValue(value, key) {
if (value === null) {
......@@ -198,10 +239,6 @@
}
});
});
finalSpec.loadbalancer.subnet = finalSpec.loadbalancer.subnet.id;
return lbaasv2API.createLoadBalancer(finalSpec);
}
function onGetLoadBalancers(response) {
......
......@@ -114,10 +114,18 @@
expect(model.listenerProtocols).toEqual(['TCP', 'HTTP', 'HTTPS']);
});
it('has array of methods', function() {
it('has array of pool methods', function() {
expect(model.methods).toEqual(['ROUND_ROBIN', 'LEAST_CONNECTIONS', 'SOURCE_IP']);
});
it('has array of monitor types', function() {
expect(model.monitorTypes).toEqual(['HTTP', 'HTTPS', 'PING', 'TCP']);
});
it('has array of monitor methods', function() {
expect(model.monitorMethods).toEqual(['GET', 'HEAD']);
});
it('has an "initialize" function', function() {
expect(model.initialize).toBeDefined();
});
......@@ -144,6 +152,7 @@
expect(model.spec.listener).toBeDefined();
expect(model.spec.pool).toBeDefined();
expect(model.spec.members).toEqual([]);
expect(model.spec.monitor).toBeDefined();
});
it('should initialize names', function() {
......@@ -151,6 +160,12 @@
expect(model.spec.listener.name).toBe('Listener 1');
expect(model.spec.pool.name).toBe('Pool 1');
});
it('should initialize monitor fields', function() {
expect(model.spec.monitor.method).toBe('GET');
expect(model.spec.monitor.status).toBe('200');
expect(model.spec.monitor.path).toBe('/');
});
});
describe('Initialization failure', function() {
......@@ -188,10 +203,11 @@
// This is here to ensure that as people add/change spec properties, they don't forget
// to implement tests for them.
it('has the right number of properties', function() {
expect(Object.keys(model.spec).length).toBe(4);
expect(Object.keys(model.spec).length).toBe(5);
expect(Object.keys(model.spec.loadbalancer).length).toBe(4);
expect(Object.keys(model.spec.listener).length).toBe(4);
expect(Object.keys(model.spec.pool).length).toBe(4);
expect(Object.keys(model.spec.monitor).length).toBe(7);
});
it('sets load balancer name to null', function() {
......@@ -241,6 +257,34 @@
it('sets pool method to null', function() {
expect(model.spec.pool.method).toBeNull();
});
it('sets monitor type to null', function() {
expect(model.spec.monitor.type).toBeNull();
});
it('sets monitor interval to null', function() {
expect(model.spec.monitor.interval).toBeNull();
});
it('sets monitor retry count to null', function() {
expect(model.spec.monitor.retry).toBeNull();
});
it('sets monitor timeout to null', function() {
expect(model.spec.monitor.timeout).toBeNull();
});
it('sets monitor method to default', function() {
expect(model.spec.monitor.method).toBe('GET');
});
it('sets monitor status code to default', function() {
expect(model.spec.monitor.status).toBe('200');
});
it('sets monitor URL path to default', function() {
expect(model.spec.monitor.path).toBe('/');
});
});
describe('Create Load Balancer', function() {
......@@ -269,6 +313,10 @@
port: 80,
weight: 1
}];
model.spec.monitor.type = 'PING';
model.spec.monitor.interval = 1;
model.spec.monitor.retry = 1;
model.spec.monitor.timeout = 1;
var finalSpec = model.createLoadBalancer();
......@@ -293,6 +341,10 @@
expect(finalSpec.members[0].id).toBeUndefined();
expect(finalSpec.members[0].name).toBeUndefined();
expect(finalSpec.members[0].description).toBeUndefined();
expect(finalSpec.monitor.type).toBe('PING');
expect(finalSpec.monitor.interval).toBe(1);
expect(finalSpec.monitor.retry).toBe(1);
expect(finalSpec.monitor.timeout).toBe(1);
});
it('should delete listener if any required property is not set', function() {
......@@ -335,6 +387,26 @@
expect(finalSpec.pool).toBeDefined();
expect(finalSpec.members).toBeUndefined();
});
it('should delete monitor if any required property not set', function() {
model.spec.loadbalancer.ip = '1.2.3.4';
model.spec.loadbalancer.subnet = model.subnets[0];
model.spec.listener.protocol = 'HTTPS';
model.spec.listener.port = 80;
model.spec.pool.protocol = 'HTTP';
model.spec.pool.method = 'LEAST_CONNECTIONS';
model.spec.monitor.type = 'PING';
model.spec.monitor.interval = 1;
model.spec.monitor.retry = 1;
var finalSpec = model.createLoadBalancer();
expect(finalSpec.loadbalancer).toBeDefined();
expect(finalSpec.listener).toBeDefined();
expect(finalSpec.pool).toBeDefined();
expect(finalSpec.members).toBeUndefined();
expect(finalSpec.monitor).toBeUndefined();
});
});
});
......
/*
* Copyright 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function () {
'use strict';
angular
.module('horizon.dashboard.project.lbaasv2.loadbalancers')
.controller('CreateMonitorController', CreateMonitorController);
CreateMonitorController.$inject = [
'horizon.dashboard.project.lbaasv2.patterns',
'horizon.framework.util.i18n.gettext'
];
/**
* @ngdoc controller
* @name CreateMonitorController
* @description
* The `CreateMonitorController` controller provides functions for
* configuring the health monitor step when creating a new health monitor.
* @param patterns The LBaaS v2 patterns constant.
* @param gettext The horizon gettext function for translation.
* @returns undefined
*/
function CreateMonitorController(patterns, gettext) {
var ctrl = this;
// Error text for invalid fields
/* eslint-disable max-len */
ctrl.intervalError = gettext('The health check interval must be greater than or equal to the timeout.');
/* eslint-enable max-len */
ctrl.retryError = gettext('The max retry count must be a number between 1 and 10.');
ctrl.timeoutError = gettext('The timeout must be a number greater than or equal to 0.');
ctrl.statusError = gettext('The expected status code is not valid.');
ctrl.pathError = gettext('The URL path is not valid.');
// Field level help text
ctrl.statusHelp = gettext('Enter comma separated values or ranges.');
ctrl.intervalHelp = gettext('The delay between health check calls.');
ctrl.retryHelp = interpolate(
/* eslint-disable max-len */
gettext('The number of allowed connection failures before changing the status of the member to %(state)s.'),
/* eslint-enable max-len */
{ state: gettext('Inactive') },
true);
// HTTP status codes validation pattern
ctrl.statusPattern = patterns.httpStatusCodes;
ctrl.urlPathPattern = patterns.urlPath;
}
})();
/*
* Copyright 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
describe('Create Monitor Step', function() {
beforeEach(module('horizon.framework.util'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
describe('CreateMonitorController', function() {
var ctrl;
beforeEach(inject(function($controller) {
ctrl = $controller('CreateMonitorController');
}));
it('should define error messages for invalid fields', function() {
expect(ctrl.intervalError).toBeDefined();
expect(ctrl.retryError).toBeDefined();
expect(ctrl.timeoutError).toBeDefined();
expect(ctrl.statusError).toBeDefined();
expect(ctrl.pathError).toBeDefined();
});
it('should define field level help messages', function() {
expect(ctrl.statusHelp).toBeDefined();
expect(ctrl.intervalHelp).toBeDefined();
expect(ctrl.retryHelp).toBeDefined();
});
it('should define patterns for field validation', function() {
expect(ctrl.statusPattern).toBeDefined();
expect(ctrl.urlPathPattern).toBeDefined();
});
});
});
})();
<h1 translate>Monitor Help</h1>
<p translate>When adding a load balancer, you can also specify a health check monitor to use to determine the health of your instances. Health checks routinely run against each instance within a target load balancer and the result of the health check is used to determine if the instance receives new connections.</p>
<div ng-controller="CreateMonitorController as ctrl">
<h1 translate>Monitor</h1>
<!--content-->
<div class="content">
<div translate class="subtitle">Provide the details for the new health monitor. The monitor will only be created if values are provided for all fields marked as required. A listener and pool are also required.</div>
<div class="row form-group">
<div class="col-sm-6 col-md-3">
<div class="form-field required monitor-type">
<label translate class="on-top" for="monitor-type">Monitor type</label>
<select class="form-control input-sm" name="monitor-type"
id="monitor-type"
ng-options="type for type in model.monitorTypes"
ng-model="model.spec.monitor.type">
</select>
</div>
</div>
</div>
<div class="row form-group">
<div class="col-sm-6 col-md-3">
<div class="form-field required monitor-interval"
ng-class="{ 'has-error': createLoadBalancerMonitorForm['monitor-interval'].$invalid && createLoadBalancerMonitorForm['monitor-interval'].$dirty }">
<label translate class="on-top" for="monitor-interval">Health check interval (sec)</label>
<span class="fa fa-exclamation-triangle invalid"
ng-show="createLoadBalancerMonitorForm['monitor-interval'].$invalid && createLoadBalancerMonitorForm.$dirty"
popover="{$ ::ctrl.intervalError $}"
popover-placement="top" popover-append-to-body="true"
popover-trigger="hover"></span>
<span class="fa fa-question-circle pull-right"
popover="{$ ::ctrl.intervalHelp $}"
popover-placement="top" popover-append-to-body="true"
popover-trigger="hover"></span>
<input name="monitor-interval" id="monitor-interval"
type="number" class="form-control input-sm"
ng-model="model.spec.monitor.interval" ng-pattern="/^\d+$/" ng-min="model.spec.monitor.timeout">
</div>
</div>
<div class="col-sm-6 col-md-3">
<div class="form-field required monitor-retry"
ng-class="{ 'has-error': createLoadBalancerMonitorForm['monitor-retry'].$invalid && createLoadBalancerMonitorForm['monitor-retry'].$dirty }">
<label translate class="on-top" for="monitor-retry">Retry count before markdown</label>
<span class="fa fa-exclamation-triangle invalid"
ng-show="createLoadBalancerMonitorForm['monitor-retry'].$invalid && createLoadBalancerMonitorForm.$dirty"
popover="{$ ::ctrl.retryError $}"
popover-placement="top" popover-append-to-body="true"
popover-trigger="hover"></span>
<span class="fa fa-question-circle pull-right"
popover="{$ ::ctrl.retryHelp $}"
popover-placement="top" popover-append-to-body="true"
popover-trigger="hover"></span>
<input name="monitor-retry" id="monitor-retry"
type="number" class="form-control input-sm"
ng-model="model.spec.monitor.retry" ng-pattern="/^\d+$/" min="1" max="10">
</div>
</div>
<div class="col-sm-6 col-md-3">
<div class="form-field required monitor-timeout"
ng-class="{ 'has-error': createLoadBalancerMonitorForm['monitor-timeout'].$invalid && createLoadBalancerMonitorForm['monitor-timeout'].$dirty }">
<label translate class="on-top" for="monitor-timeout">Timeout (sec)</label>
<span class="fa fa-exclamation-triangle invalid"
ng-show="createLoadBalancerMonitorForm['monitor-timeout'].$invalid && createLoadBalancerMonitorForm.$dirty"
popover="{$ ::ctrl.timeoutError $}"
popover-placement="top" popover-append-to-body="true"
popover-trigger="hover"></span>
<input name="monitor-timeout" id="monitor-timeout"
type="number" class="form-control input-sm"
ng-model="model.spec.monitor.timeout" ng-pattern="/^\d+$/" min="0">
</div>
</div>
</div>
<div class="row form-group" ng-if="model.spec.monitor.type === 'HTTP'">
<div class="col-sm-6 col-md-3">
<div class="form-field monitor-method">
<label translate class="on-top" for="monitor-method">HTTP method</label>
<select class="form-control input-sm" name="monitor-method"