diff --git a/README.md b/README.md index 646d863b1..17816f3c3 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,32 @@ Save the file as **.splunkrc** in the current user's home directory. Click **Yes**, then continue creating the file. +### Create/Update a .conf file +```javascript + +Async.chain([ + function (done) { + // Fetch configurations + var configs = svc.configurations(namespace); + configs.fetch(done); + }, + async function (configs, done) { + // Create a key-value map to store under a stanza + const filename = "app.conf"; + const stanzaName = "install"; + var keyValueMap = {} + keyValueMap["state"] = "enabled"; + keyValueMap["python.version"] = "python3"; + + // If file/stanza doesn't exist, it will be created + // else it will be updated. + configs.createAsync(filename, stanzaName, keyValueMap, done); + } +], +function (err) { + done(); +}); +``` ### Client-side examples diff --git a/lib/context.js b/lib/context.js index aa2980ba3..12efada12 100644 --- a/lib/context.js +++ b/lib/context.js @@ -356,19 +356,32 @@ * * @method splunkjs.Context */ - get: function(path, params, callback) { + get: function(path, params, callback, isAsync) { var that = this; - var request = function(callback) { + + if(isAsync) { return that.http.get( that.urlify(path), that._headers(), params, that.timeout, - callback + null, + true ); - }; - - return this._requestWrapper(request, callback); + } + else { + var request = function(callback) { + return that.http.get( + that.urlify(path), + that._headers(), + params, + that.timeout, + callback + ); + }; + + return this._requestWrapper(request, callback); + } }, /** diff --git a/lib/http.js b/lib/http.js index 36c52ba01..2fb686da0 100644 --- a/lib/http.js +++ b/lib/http.js @@ -133,7 +133,7 @@ * * @method splunkjs.Http */ - get: function(url, headers, params, timeout, callback) { + get: function(url, headers, params, timeout, callback, isAsync) { var message = { method: "GET", headers: headers, @@ -141,7 +141,7 @@ query: params }; - return this.request(url, message, callback); + return this.request(url, message, callback, isAsync); }, /** @@ -202,31 +202,8 @@ * @method splunkjs.Http * @see makeRequest */ - request: function(url, message, callback) { + request: function(url, message, callback, isAsync) { var that = this; - var wrappedCallback = function(response) { - callback = callback || function() {}; - - // Handle cookies if 'set-cookie' header is in the response - - var cookieHeaders = response.response.headers['set-cookie']; - if (cookieHeaders) { - utils.forEach(cookieHeaders, function (cookieHeader) { - var cookie = that._parseCookieHeader(cookieHeader); - that._cookieStore[cookie.key] = cookie.value; - }); - } - - // Handle callback - - if (response.status < 400 && response.status !== "abort") { - callback(null, response); - } - else { - callback(response); - } - }; - var query = utils.getWithVersion(this.version, queryBuilderMap)(message); var post = message.post || {}; @@ -253,7 +230,35 @@ // Now we can invoke the user-provided HTTP class, // passing in our "wrapped" callback - return this.makeRequest(encodedUrl, options, wrappedCallback); + if(isAsync) { + return this.makeRequestAsync(encodedUrl, options); + } + else { + var wrappedCallback = function(response) { + callback = callback || function() {}; + + // Handle cookies if 'set-cookie' header is in the response + + var cookieHeaders = response.response.headers['set-cookie']; + if (cookieHeaders) { + utils.forEach(cookieHeaders, function (cookieHeader) { + var cookie = that._parseCookieHeader(cookieHeader); + that._cookieStore[cookie.key] = cookie.value; + }); + } + + // Handle callback + + if (response.status < 400 && response.status !== "abort") { + callback(null, response); + } + else { + callback(response); + } + }; + + return this.makeRequest(encodedUrl, options, wrappedCallback); + } }, /** diff --git a/lib/platform/node/node_http.js b/lib/platform/node/node_http.js index 292f4bfa5..416ed65cd 100644 --- a/lib/platform/node/node_http.js +++ b/lib/platform/node/node_http.js @@ -85,6 +85,28 @@ return req; }, + makeRequestAsync: async function(url, message) { + var request_options = { + url: url, + method: message.method, + headers: message.headers || {}, + body: message.body || "", + timeout: message.timeout || 0, + jar: false, + followAllRedirects: true, + strictSSL: false, + rejectUnauthorized : false, + }; + + // Get the byte-length of the content, which adjusts for multi-byte characters + request_options.headers["Content-Length"] = Buffer.byteLength(request_options.body, "utf8"); + + var that = this; + var response = needle(request_options.method, request_options.url, request_options.body, request_options); + + return response; + }, + parseJson: function(json) { return JSON.parse(json); } diff --git a/lib/service.js b/lib/service.js index 6d88547a9..d648d12b7 100644 --- a/lib/service.js +++ b/lib/service.js @@ -717,7 +717,7 @@ * * @method splunkjs.Service.Endpoint */ - get: function(relpath, params, callback) { + get: function(relpath, params, callback, isAsync) { var url = this.qualifiedPath; // If we have a relative path, we will append it with a preceding @@ -729,7 +729,8 @@ return this.service.get( url, params, - callback + callback, + isAsync ); }, @@ -1261,6 +1262,7 @@ this._load = utils.bind(this, this._load); this.fetch = utils.bind(this, this.fetch); this.create = utils.bind(this, this.create); + this.createAsync = utils.bind(this, this.createAsync); this.list = utils.bind(this, this.list); this.item = utils.bind(this, this.item); this.instantiateEntity = utils.bind(this, this.instantiateEntity); @@ -1394,6 +1396,34 @@ return req; }, + + /** + * It's an asynchronous version of fetch(options, callback) function. + * + * Refreshes the resource by fetching the object from the server and + * loading it. + * + * @param {Object} options A dictionary of collection filtering and pagination options: + * - `count` (_integer_): The maximum number of items to return. + * - `offset` (_integer_): The offset of the first item to return. + * - `search` (_string_): The search query to filter responses. + * - `sort_dir` (_string_): The direction to sort returned items: “asc” or “desc”. + * - `sort_key` (_string_): The field to use for sorting (optional). + * - `sort_mode` (_string_): The collating sequence for sorting returned items: “auto”, “alpha”, “alpha_case”, or “num”. + * + * @method splunkjs.Service.Collection + */ + fetchAsync: async function(options) { + options = options || {}; + if (!options.count) { + options.count = 0; + } + + var that = this; + var response = await that.get("", options, null, true); + that._load(response.body); + return that; + }, /** * Returns a specific entity from the collection. @@ -3091,7 +3121,110 @@ }); return req; - } + }, + + /** + * Fetch a configuration file. + * + * @param {String} file A name for configuration file. + * @return file, if exists or null + * + * @endpoint properties + * @method splunkjs.Service.Configurations + */ + getConfFile: async function(filename) { + var that = this; + + // 1. Fetch files list + var response = await this.get("", {__conf: filename}, null, true); + + // 2. Filter the files + var files = response + && response.body + && response.body.entry + && response.body.entry.filter(f => f.name === filename); + + // 3. Check if the file exists + if(files && files.length == 0) { + return null; + } + + // 4. Create a local instance + var configurationFile = new root.ConfigurationFile(that.service, filename); + + // 5. Load the file content + var fetchedFile = await configurationFile.fetchAsync(); + + return fetchedFile; + }, + + /** + * Fetch a configuration stanza. + * + * @param {String} file A configuration file. + * @param {String} stanza A configuration stanza. + * @return stanza, if exists or null + * + * @endpoint properties + * @method splunkjs.Service.Configurations + */ + getStanza: async function(file, stanza) { + // 1. check if the stanza exists + var fetchedStanza = file.item(stanza); + + if(fetchedStanza == undefined) { + return null; + } + else { + return fetchedStanza; + } + }, + + /** + * Creates/Updates a configuration file and stanza. + * + * @param {String} filename A name for this configuration file to be created/updated. + * @param {String} stanzaName A name for the stanza to be created/updated. + * @param {String} keyValueMap A key-value map of properties to be put under the stanza. + * @param {Function} callback A function to call with the new configuration file. + * + * @endpoint properties + * @method splunkjs.Service.Configurations + */ + createAsync: async function (filename, stanzaName, keyValueMap, callback) { + callback = callback || function() {}; + var that = this; + + // 1. Check if the file exists + var configFile = await this.getConfFile(filename); + + // 2. If the file doesn't exist, create a new file + if(configFile == undefined) { + + that.create( { __conf: filename }); + + configFile = new root.ConfigurationFile( that.service, filename ); + configFile = await configFile.fetchAsync(); + } + + // 3. Check if the stanza exists + var configStanza = await this.getStanza(configFile, stanzaName); + + // 4. If the stanza doesn't exist, create a new stanza with given keyValueMap + if(configStanza == undefined) { + + configFile.create(stanzaName, keyValueMap, function (err, newStanza) { + callback(); + }); + } + + // 5. If the stanza exists, update it with the keyValueMap + else { + configStanza.update(keyValueMap, (err, updatedStanza) => { + callback(); + }); + } + }, }); /** diff --git a/tests/service_tests/configuration.js b/tests/service_tests/configuration.js index b88cded10..e62acea1c 100644 --- a/tests/service_tests/configuration.js +++ b/tests/service_tests/configuration.js @@ -139,6 +139,60 @@ exports.setup = function (svc) { }); }); + it("Callback#createAsync", function (done) { + var that = this; + var namespace = { owner: "nobody", app: "system" }; + var filename = "jssdk_file_new_" + getNextId(); + var stanza = "install" + var property1 = "state" + var value1 = "enabled"; + var property2 = "python.version" + var value2 = "python3"; + + Async.chain([ + function (done) { + var configs = svc.configurations(namespace); + configs.fetch(done); + }, + function (configs, done) { + var keyValueMap = {} + keyValueMap[property1] = value1; + keyValueMap[property2] = value2; + configs.createAsync(filename, stanza, keyValueMap, done); + }, + async function (done) { + var configs = svc.configurations(namespace); + configs.fetch(); + + // a. File exists: Positive + var configFile = await configs.getConfFile(filename); + assert.ok(configFile); + + // b. Stanza exists: Positive + configFile = await configFile.fetchAsync(); + var configStanza = await configs.getStanza(configFile, stanza); + assert.ok(configStanza); + assert.ok(configStanza._properties); + assert.strictEqual(configStanza._properties[property1], value1 ); + assert.strictEqual(configStanza._properties[property2], value2 ); + + // c. File exists: Negative + var invalidConfigFile = await configs.getConfFile("invalid_filename"); + assert.ok(!invalidConfigFile); + + // d. Stanza exists: Negative + var invalidConfigStanza = await configs.getStanza(configFile, "invalid_stanza_name"); + assert.ok(!invalidConfigStanza); + + done(); + }, + ], + function (err) { + assert.ok(!err); + done(); + }); + }); + it("Callback#can get default stanza", function (done) { var that = this; var namespace = { owner: "admin", app: "search" };