@f5devcentral/f5-cloud-libs
Advanced tools
Comparing version 4.0.0-beta.6 to 4.0.0
@@ -505,2 +505,5 @@ /** | ||
* | ||
* We only make a best effort here. If revoke fails, this still succeeds. This allows | ||
* us not to care if the license even can be revoked (perhaps it is not issued by BIG-IQ). | ||
* | ||
* @param {Object[]} instances - Instances for which to revoke licenses. Instances | ||
@@ -539,2 +542,6 @@ * should be as returned by getInstances | ||
return q.all(promises); | ||
}) | ||
.catch((err) => { | ||
this.logger.debug('Could not revoke all licenses', err); | ||
return q(); | ||
}); | ||
@@ -541,0 +548,0 @@ } |
@@ -117,3 +117,3 @@ /** | ||
.then((actualPassword) => { | ||
this.password = actualPassword; | ||
this.password = actualPassword.trim(); | ||
this.icontrol = new IControl({ | ||
@@ -314,4 +314,8 @@ host: this.host, | ||
BigIp.prototype.active = function active(retryOptions) { | ||
const retry = retryOptions || util.DEFAULT_RETRY; | ||
const retry = {}; | ||
Object.assign(retry, retryOptions || util.DEFAULT_RETRY); | ||
// While waiting for active, we may get errors but we want to keep trying | ||
retry.continueOnError = true; | ||
const func = function () { | ||
@@ -613,3 +617,3 @@ const deferred = q.defer(); | ||
.then((password) => { | ||
this.password = password; | ||
this.password = password.trim(); | ||
this.icontrol = new IControl({ | ||
@@ -792,4 +796,8 @@ host: this.host, | ||
BigIp.prototype.ready = function ready(retryOptions) { | ||
const retry = retryOptions || util.DEFAULT_RETRY; | ||
const retry = {}; | ||
Object.assign(retry, retryOptions || util.DEFAULT_RETRY); | ||
// While waiting for ready, we may get errors but we want to keep trying | ||
retry.continueOnError = true; | ||
const func = function () { | ||
@@ -796,0 +804,0 @@ const promises = []; |
@@ -334,2 +334,4 @@ /** | ||
.then((readPassword) => { | ||
const trimmedPassword = readPassword ? readPassword.trim() : ''; | ||
const func = function () { | ||
@@ -340,3 +342,3 @@ return bigIqControl.create( | ||
username: user, | ||
password: readPassword | ||
password: trimmedPassword | ||
} | ||
@@ -349,3 +351,3 @@ ); | ||
user: user.trim(), | ||
password: readPassword ? readPassword.trim() : '', | ||
password: trimmedPassword, | ||
strict: false | ||
@@ -612,3 +614,3 @@ }); | ||
// Get the current provisionalbe modules with their levels | ||
// Get the current provisionable modules with their levels | ||
response.forEach((module) => { | ||
@@ -632,15 +634,25 @@ currentProvisioning[module.name] = module.level; | ||
provisioningCommands.push({ | ||
promise: this.core.active | ||
}); | ||
provisioningCommands.push({ | ||
promise: this.core.modify, | ||
arguments: [ | ||
PROVISION_PATH + module, | ||
{ level: provisionSettings[module] }, | ||
null, | ||
{ | ||
level: provisionSettings[module] | ||
maxRetries: 90, | ||
retryIntervalMs: 10000, | ||
continueOnErrorMessage: /in progress/ | ||
} | ||
] | ||
}); | ||
provisioningCommands.push({ | ||
promise: this.core.active | ||
}); | ||
} | ||
}); | ||
// if provisioning, check for active after last module | ||
if (provisioningCommands.length > 0) { | ||
provisioningCommands.push({ | ||
promise: this.core.active | ||
}); | ||
} | ||
@@ -647,0 +659,0 @@ return util.callInSerial(this.core, provisioningCommands, DELAY_BETWEEN_PROVISION_COMMANDS); |
@@ -185,2 +185,30 @@ /** | ||
function verifyGtmServer() { | ||
const usedAddresses = []; | ||
function collectAddresses(server) { | ||
server.addresses.forEach((address) => { | ||
usedAddresses.push(address.name); | ||
}); | ||
} | ||
// when creating the server, we have to give it an IP address | ||
// we use 192.0.2.X as that is defined in https://tools.ietf.org/html/rfc5737 | ||
// for use as a documentation server and is not likely to be in use | ||
function getFirstAvailableAddress() { | ||
let highestLastOctet = 0; | ||
usedAddresses.forEach((address) => { | ||
const octets = address.split('.'); | ||
const thisLastOctet = parseInt(octets[3], 10); | ||
if (thisLastOctet > highestLastOctet) { | ||
highestLastOctet = thisLastOctet; | ||
} | ||
}); | ||
highestLastOctet += 1; | ||
if (highestLastOctet > 255) { | ||
this.logger.error('No available addresses for GTM server'); | ||
return null; | ||
} | ||
return `192.0.2.${highestLastOctet}`; | ||
} | ||
return this.bigIp.list('/tm/gtm/server') | ||
@@ -193,2 +221,4 @@ .then((servers) => { | ||
} | ||
collectAddresses(servers[i]); | ||
} | ||
@@ -201,5 +231,3 @@ } | ||
// when creating the server, we have to give it an IP address | ||
// we use 192.0.2.1 as that is defined in https://tools.ietf.org/html/rfc5737 | ||
// for use as a documentation server and is not likely to be in use | ||
const dummyAddress = getFirstAvailableAddress(); | ||
return this.bigIp.create( | ||
@@ -211,3 +239,3 @@ '/tm/gtm/server', | ||
product: 'generic-host', | ||
addresses: ['192.0.2.1'] | ||
addresses: [dummyAddress] | ||
} | ||
@@ -214,0 +242,0 @@ ); |
@@ -219,2 +219,3 @@ /** | ||
let parsedResponse; | ||
let contentType; | ||
@@ -224,2 +225,3 @@ if (responseHeaders['content-type'] | ||
) { | ||
contentType = 'application/json'; | ||
try { | ||
@@ -240,3 +242,17 @@ parsedResponse = JSON.parse(totalResponse || '{}'); | ||
if (response.statusCode >= 300) { | ||
deferred.reject(parsedResponse); | ||
let error; | ||
if (contentType === 'application/json') { | ||
if (parsedResponse.code && parsedResponse.message) { | ||
error = new Error(parsedResponse.message); | ||
error.code = parsedResponse.code; | ||
} else { | ||
error = new Error(JSON.stringify(parsedResponse)); | ||
error.code = response.statusCode; | ||
} | ||
} else { | ||
error = new Error(parsedResponse); | ||
error.code = response.statusCode; | ||
} | ||
deferred.reject(error); | ||
} else { | ||
@@ -243,0 +259,0 @@ deferred.resolve(parsedResponse); |
@@ -87,10 +87,19 @@ /** | ||
* | ||
* @param {Object} thisArg - The 'this' argument to pass to the called function | ||
* @param {Object} retryOptions - Options for retrying the request. | ||
* @param {Integer} retryOptions.maxRetries - Number of times to retry if first try fails. | ||
* 0 to not retry. Default 60. | ||
* @param {Integer} retryOptions.retryIntervalMs - Milliseconds between retries. Default 10000. | ||
* @param {Function} funcToTry - Function to try. Function should return a | ||
* Promise which is later resolved or rejected. | ||
* @param {Object[]} args - Array of arguments to pass to funcToTry | ||
* @param {Object} thisArg - The 'this' argument to pass to the | ||
* called function | ||
* @param {Object} retryOptions - Options for retrying the request. | ||
* @param {Integer} retryOptions.maxRetries - Number of times to retry if first | ||
* try fails. 0 to not retry. | ||
* Default 60. | ||
* @param {Integer} retryOptions.retryIntervalMs - Milliseconds between retries. | ||
* Default 10000. | ||
* @param {Boolean} [retryOptions.continueOnError] - Continue even if we get an | ||
* HTTP BAD_REQUEST code. Default false. | ||
* @param {String | RegExp} [retryOptions.continueOnErrorMessage] - Continue on error if the 400 error | ||
* message matches this regex | ||
* @param {Function} funcToTry - Function to try. Function should | ||
* return a Promise which is later | ||
* resolved or rejected. | ||
* @param {Object[]} args - Array of arguments to pass to | ||
* funcToTry | ||
* | ||
@@ -103,2 +112,19 @@ * @returns {Promise} A promise which is resolved with the return from funcToTry | ||
const shouldReject = function (err) { | ||
if (err && err.code === 400) { | ||
if (retryOptions.continueOnError) { | ||
return false; | ||
} | ||
if (err.message && retryOptions.continueOnErrorMessage) { | ||
let regex = retryOptions.continueOnErrorMessage; | ||
if (!(regex instanceof RegExp)) { | ||
regex = new RegExp(retryOptions.continueOnErrorMessage); | ||
} | ||
return !regex.test(err.message); | ||
} | ||
return true; | ||
} | ||
return false; | ||
}; | ||
const tryIt = function tryIt(maxRetries, interval, theFunc, deferredOrNull) { | ||
@@ -109,3 +135,3 @@ let numRemaining = maxRetries; | ||
const retryOrReject = function (err) { | ||
if (err && err.code === 400) { | ||
if (shouldReject(err)) { | ||
logger.verbose('Unrecoverable error from HTTP request. Not retrying.'); | ||
@@ -118,3 +144,8 @@ deferred.reject(err); | ||
logger.verbose('Max tries reached.'); | ||
deferred.reject(err); | ||
const originalMessage = err && err.message ? err.message : 'unknown'; | ||
const updatedError = {}; | ||
Object.assign(updatedError, err); | ||
updatedError.message = `tryUntil: max tries reached: ${originalMessage}`; | ||
updatedError.name = err && err.name ? err.name : ''; | ||
deferred.reject(updatedError); | ||
} | ||
@@ -121,0 +152,0 @@ }; |
{ | ||
"name": "@f5devcentral/f5-cloud-libs", | ||
"version": "4.0.0-beta.6", | ||
"version": "4.0.0", | ||
"description": "Common library code and scripts for deploying a BIG-IP in a cloud environment", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
# Release notes | ||
## Version 4.0.0 | ||
**This version is not backwards compatible. The install location of `f5-cloud-libs` | ||
and `f5-cloud-libs-<cloud>` has changed to support installation from npm** | ||
* Scripts now exit with status code 1 on failure | ||
* Support autoscaling with BYOL and utility billing instances in one cluster | ||
* Support autoscaling with DNS updates | ||
* Support for automatic backups in autoscaling solutions | ||
* Update scripts and lib code to the [Airbnb JavaScript style guide](https://github.com/airbnb/javascript) | ||
* Provide independent licensing script callable from tmsh | ||
* Add options for CLPv2 when licensing via BIG-IQ | ||
## Version 3.6.0 | ||
@@ -52,4 +64,4 @@ * Add --shell option to scripts/runScript.js | ||
* Allows for autoscaling and clustering without providing a password in the template | ||
* Adds hash verification for all downloaded files | ||
* Fixes race condition when running multiple f5-cloud-libs scripts at once | ||
* Add hash verification for all downloaded files | ||
* Fix race condition when running multiple f5-cloud-libs scripts at once | ||
@@ -59,4 +71,4 @@ ## Version 2.0.0 | ||
* All scripts that take --password now also support --password-url. Only 'file' URLs are supported for now. | ||
* Added option to suppress console output (--no-console). | ||
* Added support for verifying hash of downloaded f5-cloud-libs tarball. | ||
* Added some parsing of sync messages to get sync to work more often. | ||
* Add option to suppress console output (--no-console). | ||
* Add support for verifying hash of downloaded f5-cloud-libs tarball. | ||
* Add some parsing of sync messages to get sync to work more often. |
@@ -55,2 +55,3 @@ /** | ||
let rebooting; | ||
let exiting; | ||
@@ -386,2 +387,3 @@ Object.assign(optionsForTest, testOpts); | ||
util.logAndExit(`Cluster failed: ${message}`, 'error', 1); | ||
exiting = true; | ||
return q(); | ||
@@ -395,2 +397,8 @@ }) | ||
ipc.send(options.signal || signals.CLUSTER_DONE); | ||
if (!exiting) { | ||
util.logAndExit('Cluster finished.'); | ||
} | ||
} else if (!options.reboot) { | ||
// If we are rebooting, but we were called with --no-reboot, send signal | ||
ipc.send(options.signal || signals.CLUSTER_DONE); | ||
} | ||
@@ -401,4 +409,2 @@ | ||
} | ||
util.logAndExit('Cluster finished.'); | ||
}); | ||
@@ -405,0 +411,0 @@ |
@@ -36,3 +36,3 @@ /** | ||
return options | ||
.version('4.0.0-beta.6') | ||
.version('4.0.0') | ||
.option( | ||
@@ -39,0 +39,0 @@ '--host <ip_address>', |
@@ -43,3 +43,3 @@ /** | ||
options | ||
.version('4.0.0-beta.6') | ||
.version('4.0.0') | ||
.option('--data-file <data_file>', 'Full path to file with data (use this or --data)') | ||
@@ -46,0 +46,0 @@ .parse(argv); |
@@ -58,3 +58,3 @@ /** | ||
options | ||
.version('4.0.0-beta.6') | ||
.version('4.0.0') | ||
.option( | ||
@@ -61,0 +61,0 @@ '--background', |
@@ -36,3 +36,3 @@ /** | ||
options | ||
.version('4.0.0-beta.6') | ||
.version('4.0.0') | ||
.option( | ||
@@ -39,0 +39,0 @@ '--length <password_length>', |
@@ -64,3 +64,3 @@ /** | ||
options | ||
.version('4.0.0-beta.6') | ||
.version('4.0.0') | ||
.option( | ||
@@ -67,0 +67,0 @@ '--host <ip_address>', |
@@ -69,2 +69,3 @@ /** | ||
let rebooting; | ||
let exiting; | ||
let index; | ||
@@ -584,2 +585,3 @@ | ||
util.logAndExit(`Onboard failed: ${message}`, 'error', 1); | ||
exiting = true; | ||
return q(); | ||
@@ -590,8 +592,2 @@ }) | ||
if (!rebooting) { | ||
util.deleteArgs(ARGS_FILE_ID); | ||
} | ||
ipc.send(options.signal || signals.ONBOARD_DONE); | ||
if (cb) { | ||
@@ -601,3 +597,12 @@ cb(); | ||
util.logAndExit('Onboard finished.'); | ||
if (!rebooting) { | ||
util.deleteArgs(ARGS_FILE_ID); | ||
ipc.send(options.signal || signals.ONBOARD_DONE); | ||
if (!exiting) { | ||
util.logAndExit('Onboard finished.'); | ||
} | ||
} else if (!options.reboot) { | ||
// If we are rebooting, but we were called with --no-reboot, send signal | ||
ipc.send(options.signal || signals.ONBOARD_DONE); | ||
} | ||
}); | ||
@@ -604,0 +609,0 @@ |
@@ -51,3 +51,3 @@ /** | ||
options | ||
.version('4.0.0-beta.6') | ||
.version('4.0.0') | ||
.option( | ||
@@ -54,0 +54,0 @@ '--background', |
@@ -417,4 +417,37 @@ /** | ||
}); | ||
}, | ||
testRevokeFail: function(test) { | ||
var instances = [ | ||
{ | ||
hostname: 'host1' | ||
}, | ||
{ | ||
hostname: 'host2' | ||
} | ||
]; | ||
var licensePool = 'myLicensePool'; | ||
testAutoscaleProvider.clOptions = { | ||
licensePool: true, | ||
licensePoolName: licensePool | ||
}; | ||
bigIqMock.revokeLicense = function(poolName, hostname) { | ||
return q.reject(new Error('foo')); | ||
} | ||
test.expect(1); | ||
testAutoscaleProvider.revokeLicenses(instances, {}) | ||
.then(function() { | ||
test.ok(true); | ||
}) | ||
.catch(function(err) { | ||
test.ok(false, 'Revoke should not throw'); | ||
}) | ||
.finally(function() { | ||
test.done(); | ||
}); | ||
} | ||
} | ||
}; |
@@ -238,2 +238,46 @@ /** | ||
testServerCreatedAddressInUse: function(test) { | ||
icontrolMock.when( | ||
'list', | ||
'/tm/gtm/server', | ||
[ | ||
{ | ||
name: 'myOtherServer', | ||
addresses: [ | ||
{name: '192.0.2.1'}, | ||
{name: '192.0.2.2'} | ||
] | ||
}, | ||
{ | ||
name: 'myThirdServer', | ||
addresses: [ | ||
{name: '192.0.2.3'} | ||
] | ||
} | ||
]); | ||
test.expect(1); | ||
gtmDnsProvider.init(providerOptions) | ||
.then(function() { | ||
return gtmDnsProvider.update(instances); | ||
}) | ||
.then(function() { | ||
test.deepEqual( | ||
icontrolMock.getRequest('create', '/tm/gtm/server'), | ||
{ | ||
name: 'myServer', | ||
datacenter: 'myDatacenter', | ||
product: 'generic-host', | ||
addresses: [ '192.0.2.4' ] | ||
} | ||
); | ||
}) | ||
.catch(function(err) { | ||
test.ok(false, err); | ||
}) | ||
.finally(function() { | ||
test.done(); | ||
}); | ||
}, | ||
testServerNotCreated: function(test) { | ||
@@ -240,0 +284,0 @@ icontrolMock.when( |
@@ -74,3 +74,4 @@ /** | ||
testBadStatusCode: function(test) { | ||
httpMock.setResponse({foo: 'bar'}, {'Content-Type': 'application/json'}, 300); | ||
const errorCode = 300; | ||
httpMock.setResponse({foo: 'bar'}, {'Content-Type': 'application/json'}, errorCode); | ||
iControl.list('somepath') | ||
@@ -80,4 +81,4 @@ .then(function() { | ||
}) | ||
.catch(function() { | ||
test.ok(true); | ||
.catch(function(err) { | ||
test.strictEqual(err.code, errorCode); | ||
}) | ||
@@ -84,0 +85,0 @@ .finally(function() { |
@@ -1329,2 +1329,3 @@ /** | ||
testBadRequest: function(test) { | ||
const errorMessage = 'foo'; | ||
var func = function() { | ||
@@ -1334,3 +1335,3 @@ return q.reject( | ||
code: 400, | ||
message: 'foo' | ||
message: errorMessage | ||
} | ||
@@ -1345,4 +1346,4 @@ ) | ||
}) | ||
.catch(function() { | ||
test.ok(true); | ||
.catch(function(err) { | ||
test.strictEqual(err.message, errorMessage); | ||
}) | ||
@@ -1352,2 +1353,94 @@ .finally(function() { | ||
}); | ||
}, | ||
testContinueOnError: function(test) { | ||
var func = function() { | ||
return q.reject( | ||
{ | ||
code: 400, | ||
message: 'foo' | ||
} | ||
) | ||
}; | ||
test.expect(1); | ||
util.tryUntil(this, {maxRetries: 2, retryIntervalMs: 10, continueOnError: true}, func) | ||
.then(function() { | ||
test.ok(false, 'func should never have resolved'); | ||
}) | ||
.catch(function(err) { | ||
test.notStrictEqual(err.message.indexOf('max tries'), -1); | ||
}) | ||
.finally(function() { | ||
test.done(); | ||
}); | ||
}, | ||
testContinueOnErrorMessageIsMessage: function(test) { | ||
var func = function() { | ||
return q.reject( | ||
{ | ||
code: 400, | ||
message: 'is foo' | ||
} | ||
) | ||
}; | ||
test.expect(1); | ||
util.tryUntil(this, {maxRetries: 2, retryIntervalMs: 10, continueOnErrorMessage: 'foo'}, func) | ||
.then(function() { | ||
test.ok(false, 'func should never have resolved'); | ||
}) | ||
.catch(function(err) { | ||
test.notStrictEqual(err.message.indexOf('max tries'), -1); | ||
}) | ||
.finally(function() { | ||
test.done(); | ||
}); | ||
}, | ||
testContinueOnErrorMessageIsMessageRegex: function(test) { | ||
var func = function() { | ||
return q.reject( | ||
{ | ||
code: 400, | ||
message: 'is foo' | ||
} | ||
) | ||
}; | ||
test.expect(1); | ||
util.tryUntil(this, {maxRetries: 2, retryIntervalMs: 10, continueOnErrorMessage: /foo/}, func) | ||
.then(function() { | ||
test.ok(false, 'func should never have resolved'); | ||
}) | ||
.catch(function(err) { | ||
test.notStrictEqual(err.message.indexOf('max tries'), -1); | ||
}) | ||
.finally(function() { | ||
test.done(); | ||
}); | ||
}, | ||
testContinueOnErrorMessageIsNotMessage: function(test) { | ||
var func = function() { | ||
return q.reject( | ||
{ | ||
code: 400, | ||
message: 'is foo' | ||
} | ||
) | ||
}; | ||
test.expect(1); | ||
util.tryUntil(this, {maxRetries: 2, retryIntervalMs: 10, continueOnErrorMessage: 'bar'}, func) | ||
.then(function() { | ||
test.ok(false, 'func should never have resolved'); | ||
}) | ||
.catch(function(err) { | ||
test.strictEqual(err.message.indexOf('max tries'), -1); | ||
}) | ||
.finally(function() { | ||
test.done(); | ||
}); | ||
} | ||
@@ -1354,0 +1447,0 @@ }, |
@@ -816,2 +816,22 @@ /** | ||
}; | ||
const instance1 = new AutoscaleInstance() | ||
.setHostname('host1') | ||
.setPrivateIp('1.2.3.4') | ||
.setMgmtIp('1.2.3.4'); | ||
const instance2 = new AutoscaleInstance() | ||
.setIsMaster() | ||
.setHostname('host2') | ||
.setPrivateIp('5.6.7.8') | ||
.setMgmtIp('5.6.7.8'); | ||
instance2.masterStatus = { | ||
instanceId: "two" | ||
}; | ||
instances = { | ||
"one": instance1, | ||
"two": instance2 | ||
}; | ||
callback(); | ||
@@ -818,0 +838,0 @@ }, |
10
USAGE.md
@@ -204,1 +204,11 @@ # Usage | ||
-h, --help output usage information | ||
## Standalone licensing | ||
### Install | ||
admin@(bigip1)(cfg-sync Standalone)(NO LICENSE)(/Common)(tmos)# run util bash -c "mkdir -p /config/licensing; cd /config/licensing; npm --loglevel=error install @f5devcentral/f5-cloud-libs" | ||
### License from BIG-IQ | ||
admin@(bigip1)(cfg-sync Standalone)(NO LICENSE)(/Common)(tmos)# license password <big_ip_admin_password> big-iq-host <big_iq_ip_address> big-iq-user <big_iq_admin_user> big-iq-password <big_iq_admin_password> license-pool-name <license_pool> | ||
### Other licensing options | ||
admin@(bigip1)(cfg-sync Standalone)(NO LICENSE)(/Common)(tmos)# license help |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
998816
23134
1