From 7ee22fdfbefba7d5b7ba3c61baa9a96d9934b453 Mon Sep 17 00:00:00 2001 From: Michael Krotscheck Date: Wed, 14 Sep 2016 11:51:03 -0700 Subject: [PATCH] Created Nova service with flavorList method. We can now query for flavors. Change-Id: Ia19afa530ae8c2df60db740cec9267fe240978c0 --- configure-devstack.js | 3 +- src/nova.js | 72 +++++++++++++ test/functional/novaTest.js | 74 +++++++++++++ test/unit/helpers/data/nova.js | 187 +++++++++++++++++++++++++++++++++ test/unit/novaTest.js | 75 +++++++++++++ vagrant.sh | 4 + 6 files changed, 414 insertions(+), 1 deletion(-) create mode 100644 src/nova.js create mode 100644 test/functional/novaTest.js create mode 100644 test/unit/helpers/data/nova.js create mode 100644 test/unit/novaTest.js diff --git a/configure-devstack.js b/configure-devstack.js index b5d2661..d385ea3 100644 --- a/configure-devstack.js +++ b/configure-devstack.js @@ -8,7 +8,8 @@ function getDevstackConfig() { return getCorsConfig('$KEYSTONE_CONF', karmaConfig) + getCorsConfig('$GLANCE_API_CONF', karmaConfig) + - getCorsConfig('$NEUTRON_CONF', karmaConfig); + getCorsConfig('$NEUTRON_CONF', karmaConfig) + + getCorsConfig('$NOVA_CONF', karmaConfig); } diff --git a/src/nova.js b/src/nova.js new file mode 100644 index 0000000..f2aae20 --- /dev/null +++ b/src/nova.js @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2016 Michael Krotscheck. + * + * 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. + */ + +import AbstractService from "./util/abstractService"; + +/** + * A list of all supported versions. Please keep this array sorted by most recent. + * + * @type {Array} + * @ignore + */ +const supportedNovaVersions = [ + 'v2.1' +]; + +export default class Nova extends AbstractService { + + /** + * This class provides direct, idempotent, low-level access to the Nova API of a specific + * cloud. The constructor requires that you provide a specific nova interface endpoint + * descriptor, as received from keystone's catalog list. + * + * @example + * { + * region_id: "RegionOne", + * url: "http://127.0.0.1:8774/", + * region: "RegionOne", + * interface: "admin", + * id: "0b8b5f0f14904136ab5a4f83f27ec49a" + * } + * @param {{}} endpointConfig The configuration element for a specific nova endpoint. + */ + constructor(endpointConfig) { + // Sanity checks. + if (!endpointConfig || !endpointConfig.url) { + throw new Error('An endpoint configuration is required.'); + } + // Clone the config, so that this instance is immutable + // at runtime (no modifying the config after the fact). + endpointConfig = Object.assign({}, endpointConfig); + + super(endpointConfig.url, supportedNovaVersions); + this._config = endpointConfig; + } + + /** + * List the flavors available on nova. + * + * @param {String} token An authorization token, or a promise which will resolve into one. + * @returns {Promise.} A promise which will resolve with the list of flavors. + */ + flavorList(token = null) { + return this + ._requestComponents(token) + .then(([url, headers]) => this.http.httpRequest('GET', `${url}flavors`, headers)) + .then((response) => response.json()) + .then((body) => body.flavors); + } +} diff --git a/test/functional/novaTest.js b/test/functional/novaTest.js new file mode 100644 index 0000000..de7adc7 --- /dev/null +++ b/test/functional/novaTest.js @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2016 Michael Krotscheck. + * + * 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. + */ + +import config from "./helpers/cloudsConfig"; +import Version from '../../src/util/version'; +import Nova from "../../src/nova"; +import Keystone from "../../src/keystone"; +import log from 'loglevel'; + +log.setLevel("DEBUG"); + +describe("Nova", () => { + // Create a keystone instance and extract the nova API endpoint. + let devstackConfig = config.clouds.devstack; + let keystone = new Keystone(devstackConfig); + let tokenPromise = keystone.tokenIssue(); + + let configPromise = tokenPromise + .then((token) => keystone.catalogList(token)) + .then((catalog) => catalog.find((entry) => entry.name === 'nova')) + .then((entry) => entry.endpoints.find((endpoint) => endpoint.interface === 'public')); + + describe("version()", () => { + const supportedApiVersions = [ + new Version('2.1') + ]; + + /** + * This test acts as a canary, to inform the SDK developers that the Nova API + * has changed in a significant way. + */ + it("should return a supported version.", (done) => { + configPromise + .then((config) => new Nova(config)) + .then((nova) => nova.version()) + .then((apiVersion) => { + let found = supportedApiVersions.find((item) => item.equals(apiVersion)); + expect(found).not.toBeFalsy(); + done(); + }) + .catch((error) => done.fail(error)); + }); + }); + + describe("flavorList()", () => { + + /** + * Assert that we can get a list of flavors. + */ + it("should return a list of flavors.", (done) => { + configPromise + .then((config) => new Nova(config)) + .then((nova) => nova.flavorList(tokenPromise)) + .then((flavors) => { + expect(flavors.length > 0).toBeTruthy(); + done(); + }) + .catch((error) => done.fail(error)); + }); + }); +}); diff --git a/test/unit/helpers/data/nova.js b/test/unit/helpers/data/nova.js new file mode 100644 index 0000000..aedacb9 --- /dev/null +++ b/test/unit/helpers/data/nova.js @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2016 Hewlett Packard Enterprise Development L.P. + * + * 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. + */ + +/** + * This file contains test data for fetchMock, to simplify bootstrapping of unit tests for + * nova. Most of these are functions, as FetchMock does not perform a safe clone of the + * instances, and may accidentally modify them at runtime. + */ + +/** + * A catalog entry that matches what we expect from the Keystone Catalog for nova compute. + */ +const novaConfig = { + region_id: "RegionOne", + url: "http://192.168.99.99:8774/v2.1", + region: "RegionOne", + interface: "public", + id: "be681632633d4a62a781148c2fedd6aa" +}; + +/** + * Build a new FetchMock configuration for the root endpoint. + * + * @returns {{}} A full FetchMock configuration for Nova's Root Resource. + */ +function rootResponse() { + return { + method: 'GET', + matcher: 'http://192.168.99.99:8774/', + response: { + versions: [{ + status: "CURRENT", + updated: "2013-07-23T11:33:21Z", + links: [{href: "http://192.168.99.99:8774/v2.1/", rel: "self"}], + min_version: "2.1", + version: "2.38", + id: "v2.1" + }, { + status: "SUPPORTED", + updated: "2011-01-21T11:33:21Z", + links: [{href: "http://192.168.99.99:8774/v2/", rel: "self"}], + min_version: "", + version: "", + id: "v2.0" + }] + } + }; +} + +/** + * Create a FAILING response to the version endpoint. + * + * @param {String} version The version ID. + * @return {{}} A FetchMock configuration for this request's response. + */ +function versionedRootResponse(version = 'v2.1') { + return { + method: 'GET', + matcher: `http://192.168.99.99:8774/${version}`, + response: { + status: 401 + } + }; +} + +/** + * Simulate an imageList response. + * + * @param {String} token An auth token. + * @return {{}} A FetchMock configuration for this request's response. + */ +function flavorList(token) { + return { + method: 'GET', + matcher: 'http://192.168.99.99:8774/v2.1/flavors', + headers: { + 'X-Auth-Token': token + }, + response: { + flavors: [{ + id: "1", + links: [ + {href: "http://192.168.99.99:8774/v2.1/flavors/1", rel: "self"}, + {href: "http://192.168.99.99:8774/flavors/1", rel: "bookmark"} + ], + name: "m1.tiny" + }, { + id: "2", + links: [ + {href: "http://192.168.99.99:8774/v2.1/flavors/2", rel: "self"}, + {href: "http://192.168.99.99:8774/flavors/2", rel: "bookmark"} + ], + name: "m1.small" + }, { + id: "3", + links: [ + {href: "http://192.168.99.99:8774/v2.1/flavors/3", rel: "self"}, + {href: "http://192.168.99.99:8774/flavors/3", rel: "bookmark"} + ], + name: "m1.medium" + }, { + id: "4", + links: [ + {href: "http://192.168.99.99:8774/v2.1/flavors/4", rel: "self"}, + {href: "http://192.168.99.99:8774/flavors/4", rel: "bookmark"} + ], + name: "m1.large" + }, { + id: "42", + links: [ + {href: "http://192.168.99.99:8774/v2.1/flavors/42", rel: "self"}, + {href: "http://192.168.99.99:8774/flavors/42", rel: "bookmark"} + ], + name: "m1.nano" + }, { + id: "5", + links: [ + {href: "http://192.168.99.99:8774/v2.1/flavors/5", rel: "self"}, + {href: "http://192.168.99.99:8774/flavors/5", rel: "bookmark"} + ], + name: "m1.xlarge" + }, { + id: "84", + links: [ + {href: "http://192.168.99.99:8774/v2.1/flavors/84", rel: "self"}, + {href: "http://192.168.99.99:8774/flavors/84", rel: "bookmark"} + ], + name: "m1.micro" + }, { + id: "c1", + links: [ + {href: "http://192.168.99.99:8774/v2.1/flavors/c1", rel: "self"}, + {href: "http://192.168.99.99:8774/flavors/c1", rel: "bookmark"} + ], + name: "cirros256" + }, { + id: "d1", + links: [ + {href: "http://192.168.99.99:8774/v2.1/flavors/d1", rel: "self"}, + {href: "http://192.168.99.99:8774/flavors/d1", rel: "bookmark"} + ], + name: "ds512M" + }, { + id: "d2", + links: [ + {href: "http://192.168.99.99:8774/v2.1/flavors/d2", rel: "self"}, + {href: "http://192.168.99.99:8774/flavors/d2", rel: "bookmark"} + ], + name: "ds1G" + }, { + id: "d3", + links: [ + {href: "http://192.168.99.99:8774/v2.1/flavors/d3", rel: "self"}, + {href: "http://192.168.99.99:8774/flavors/d3", rel: "bookmark"} + ], + name: "ds2G" + }, { + id: "d4", + links: [ + {href: "http://192.168.99.99:8774/v2.1/flavors/d4", rel: "self"}, + {href: "http://192.168.99.99:8774/flavors/d4", rel: "bookmark"} + ], + name: "ds4G" + }] + } + }; +} + +export { + novaConfig as config, + rootResponse as root, + versionedRootResponse as rootVersion, + flavorList +}; diff --git a/test/unit/novaTest.js b/test/unit/novaTest.js new file mode 100644 index 0000000..57bff95 --- /dev/null +++ b/test/unit/novaTest.js @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2016 Michael Krotscheck. + * + * 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. + */ + +import Nova from "../../src/nova.js"; +import * as mockData from "./helpers/data/nova"; +import fetchMock from "fetch-mock"; + +describe('Nova', () => { + + afterEach(fetchMock.restore); + + it('should export a class', () => { + const nova = new Nova(mockData.config); + expect(nova).toBeDefined(); + }); + + it('should throw an error for an empty config', () => { + expect(() => new Nova(null)).toThrow(); + }); + + describe("flavorList()", () => { + let nova = null; + + beforeEach(() => { + fetchMock.mock(mockData.rootVersion()); + fetchMock.mock(mockData.root()); + nova = new Nova(mockData.config); + }); + + it("should return the flavors as an array.", (done) => { + const token = 'test_token'; + + fetchMock.mock(mockData.flavorList(token)); + nova + .flavorList(token) + .then((images) => { + expect(images.length).not.toBe(0); + done(); + }) + .catch((error) => done.fail(error)); + }); + + it("Should not cache its results", (done) => { + const token = 'test_token'; + + let mockOptions = mockData.flavorList(token); + fetchMock.mock(mockOptions); + + nova + .flavorList(token) + .then(() => { + expect(fetchMock.calls(mockOptions.matcher).length).toEqual(1); + return nova.flavorList(token); + }) + .then(() => { + expect(fetchMock.calls(mockOptions.matcher).length).toEqual(2); + done(); + }) + .catch((error) => done.fail(error)); + }); + }); +}); diff --git a/vagrant.sh b/vagrant.sh index e7c7d85..a803070 100644 --- a/vagrant.sh +++ b/vagrant.sh @@ -54,6 +54,10 @@ allowed_origin=http://localhost:9876 [[post-config|\$NEUTRON_CONF]] [cors] allowed_origin=http://localhost:9876 + +[[post-config|\$NOVA_CONF]] +[cors] +allowed_origin=http://localhost:9876 EOL # Start devstack.