openvibers/openvibe.host Commits

Host: OVHcloud VPS plans from Sources (unmetered at their port speed), a third provider (#97)

OP OpenVibers committed 29 days ago · c243e60132 · 3 files, +40 −3

**Host maps OVHcloud's VPS plans from Sources (`ovhcloud-vps`), a third provider beside Linode and Amazon Lightsail.**

**Why:** openvibe.host compares 95 plans from two providers. Vultr's API disallows robots and the other keyless APIs disallow robots or need keys. OVHcloud's public order catalog is marked `noAuthentication` in its own API schema and publishes no robots rules. Sources reads it as `ovhcloud-vps` (Sources#49): the current plan codes only, each with its product's hardware and its monthly no-commitment price.

**Mapping (`server/catalog/mapping.js`):**
- The provider is `ovhcloud`, linking https://us.ovhcloud.com/vps/.
- The catalog's price is in hundred-millionths of the currency (535000000 → $5.35), and a non-USD catalog is refused.
- Memory GB → MB. The disk type is NVMe when the interface says so, otherwise SSD when the technology says so.
- OVHcloud VPS traffic is unmetered at the plan's bandwidth, which is a port speed: `unmetered: true`, `port_mbps` from the catalog, and `bandwidth_gb` null, never an invented allowance.
- There is no hourly price.

**Store (`server/catalog/store.js`):** sourced plans now write `unmetered` and `port_mbps`. The columns, the plan page ("Transfer: Unmetered", "Port: 500 Mbps") and search already had them. Linode, Vultr and Lightsail write null as before.

**Test (`catalog-ingest.test.js`):** a VPS-1 2027 maps to 2 vCPU, 4096 MB, 40 GB NVMe, unmetered at 500 Mbps, $5.35, with no hourly price. A EUR catalog and a missing price throw, and a zero price is held. The applied row reads back with `unmetered` and `port_mbps`. The 11 test files that reach the change pass.

Plans appear once Sources enables `ovhcloud-vps` (seeded disabled, enabled after its notes are reviewed).

Changes

ddiff --git a/server/catalog/mapping.js b/server/catalog/mapping.js
iindex 649bb8a..164a691 100644
---- a/server/catalog/mapping.js
++++ b/server/catalog/mapping.js
@@@ -6,6 +6,7 @@ const SOURCES = Object.freeze({
'vultr-plans': { slug: 'vultr', name: 'Vultr', homepage_url: 'https://www.vultr.com/', source_url: 'https://www.vultr.com/pricing/' },
'linode-types': { slug: 'linode', name: 'Linode', homepage_url: 'https://www.linode.com/', source_url: 'https://www.linode.com/pricing/' },
'aws-lightsail-bundles': { slug: 'aws-lightsail', name: 'Amazon Lightsail', homepage_url: 'https://aws.amazon.com/lightsail/', source_url: 'https://aws.amazon.com/lightsail/pricing/' },
++ 'ovhcloud-vps': { slug: 'ovhcloud', name: 'OVHcloud', homepage_url: 'https://us.ovhcloud.com/', source_url: 'https://us.ovhcloud.com/vps/' },
});
const number = (v, field) => {
if (v == null) return null;
@@@ -45,7 +46,27 @@ function lightsail(x, id) {
// Lightsail bills a bundle by the hour up to its monthly price, which is the hourly rate for 744 hours (a 31-day
// month): 0.00672 an hour is the $5.00 bundle AWS lists.
price_monthly: Math.round(hourly * 744 * 100) / 100, price_hourly: hourly,
-- ipv4: ipv6Only ? 0 : 1, locations: typeof x.location === 'string' && x.location ? [x.location] : null,
++ ipv4: ipv6Only ? 0 : 1, locations: typeof x.location === 'string' && x.location ? [x.location] : null, unmetered: null, port_mbps: null,
++ };
++}
++// OVHcloud's public catalog (Sources `ovhcloud-vps`): the plan's hardware joined from its product, and its monthly
++// no-commitment price in hundred-millionths of the currency (535000000 is the $5.35 VPS-1). Traffic is unmetered at
++// the plan's bandwidth, which is a port speed in Mbit/s, not a transfer allowance.
++function ovhcloud(x, id, title) {
++ if (x.currency !== 'USD') throw new TypeError('OVHcloud price is not in USD');
++ const units = number(x.price_monthly, 'monthly price');
++ if (units == null) throw new TypeError('missing monthly price');
++ const monthly = Math.round(units / 1e6) / 100;
++ if (monthly <= 0) return null;
++ const memoryGb = number(x.memory_gb, 'memory');
++ const unmetered = x.bandwidth_unlimited === true;
++ return {
++ name: (typeof title === 'string' && title.trim()) || id, kind: 'vps',
++ vcpu: number(x.vcpu, 'vcpu'), ram_mb: memoryGb == null ? null : Math.round(memoryGb * 1024),
++ disk_gb: number(x.disk_gb, 'disk'),
++ disk_type: /nvme/i.test(String(x.disk_interface || '')) ? 'nvme' : /ssd/i.test(String(x.disk_technology || '')) ? 'ssd' : null,
++ bandwidth_gb: null, unmetered: unmetered || null, port_mbps: number(x.bandwidth_mbps, 'bandwidth'),
++ price_monthly: monthly, price_hourly: null, ipv4: null, locations: null,
};
}
// Vultr's plan types as its pricing page names them; a plan is named from its type, CPU vendor, vCPUs and memory
@@@ -72,6 +93,10 @@ function mapItem(item) {
source_url: provider.source_url, checked_at: Date.parse(item.provenance.retrieved_at), first_seen_at: Date.parse(item.provenance.first_seen_at),
};
if (item.source_key === 'aws-lightsail-bundles') return { ...common, ...lightsail(x, id) };
++ if (item.source_key === 'ovhcloud-vps') {
++ const plan = ovhcloud(x, id, item.title);
++ return plan && { ...common, ...plan };
++ }
const vultr = item.source_key === 'vultr-plans';
const monthly = number(vultr ? x.monthly_cost : x.price_monthly, 'monthly price');
if (monthly == null) throw new TypeError('missing monthly price');
@@@ -87,7 +112,7 @@ function mapItem(item) {
disk_type: vultr ? (['vhf', 'vhp'].includes(x.type) ? 'nvme' : x.type === 'vc2' ? 'ssd' : null) : null,
bandwidth_gb: number(vultr ? x.bandwidth : x.transfer, 'bandwidth'),
price_monthly: monthly, price_hourly: number(vultr ? x.hourly_cost : x.price_hourly, 'hourly price'),
-- ipv4: null, locations: null,
++ ipv4: null, locations: null, unmetered: null, port_mbps: null,
};
}
module.exports = { SOURCES, mapItem };
ddiff --git a/server/catalog/store.js b/server/catalog/store.js
iindex 259f46e..e3ca28e 100644
---- a/server/catalog/store.js
++++ b/server/catalog/store.js
@@@ -115,7 +115,7 @@ function createCatalog({ store, community = null, log = console }) {
p = { id };
}
const old = await t.prepare('SELECT * FROM catalog_plans WHERE provider_id = ? AND external_id = ?').get(p.id, v.external_id);
-- const cols = ['name', 'kind', 'vcpu', 'ram_mb', 'disk_gb', 'disk_type', 'bandwidth_gb', 'ipv4', 'locations', 'currency', 'price_monthly', 'price_hourly', 'source_key', 'source_item_id', 'source_url', 'checked_at'];
++ const cols = ['name', 'kind', 'vcpu', 'ram_mb', 'disk_gb', 'disk_type', 'bandwidth_gb', 'unmetered', 'port_mbps', 'ipv4', 'locations', 'currency', 'price_monthly', 'price_hourly', 'source_key', 'source_item_id', 'source_url', 'checked_at'];
if (old) {
if (old.origin !== 'sources' || v.checked_at < Number(old.checked_at)) return 'hold';
if (Number(old.price_monthly) !== v.price_monthly || old.currency !== v.currency) {
ddiff --git a/test/catalog-ingest.test.js b/test/catalog-ingest.test.js
iindex 9961d1e..7d590a9 100644
---- a/test/catalog-ingest.test.js
++++ b/test/catalog-ingest.test.js
@@@ -53,6 +53,18 @@ const item = (key, id, fields, time = at, title = id) => ({ id: `itm_${'0'.repea
const row = (await catalog.plans({ provider: 'aws-lightsail' })).plans[0];
assert.deepStrictEqual([row.name, Number(row.price_monthly), row.ipv4, row.locations], ['General purpose 0.5 GB', 5, 1, ['US East (N. Virginia)']]);
});
++ await check('an OVHcloud plan maps from the catalog: hundred-millionths to dollars, unmetered at its port speed', async () => {
++ const f = { product: 'vps-2027-model1', currency: 'USD', vcpu: 2, cpu_type: 'vCore', memory_gb: 4, disk_gb: 40, disk_interface: 'NVMe', disk_technology: 'SSD', bandwidth_mbps: 500, bandwidth_unlimited: true, price_monthly: 535000000 };
++ const p = mapItem(item('ovhcloud-vps', 'vps-2027-model1', f, at, 'VPS-1 2027'));
++ assert.deepStrictEqual([p.provider.slug, p.name, p.kind, p.vcpu, p.ram_mb, p.disk_gb, p.disk_type, p.bandwidth_gb, p.unmetered, p.port_mbps, p.price_monthly, p.price_hourly, p.source_url],
++ ['ovhcloud', 'VPS-1 2027', 'vps', 2, 4096, 40, 'nvme', null, true, 500, 5.35, null, 'https://us.ovhcloud.com/vps/']);
++ assert.throws(() => mapItem(item('ovhcloud-vps', 'x', { ...f, currency: 'EUR' })), /not in USD/);
++ assert.throws(() => mapItem(item('ovhcloud-vps', 'x', { ...f, price_monthly: null })), /missing monthly price/);
++ assert.strictEqual(mapItem(item('ovhcloud-vps', 'x', { ...f, price_monthly: 0 })), null, 'a zero price is not listed');
++ await catalog.applyItem(item('ovhcloud-vps', 'vps-2027-model1', f, at, 'VPS-1 2027'));
++ const row = (await catalog.plans({ provider: 'ovhcloud' })).plans[0];
++ assert.deepStrictEqual([row.name, Number(row.price_monthly), row.unmetered, Number(row.port_mbps), row.bandwidth_gb], ['VPS-1 2027', 5.35, true, 500, null]);
++ });
await check('upsert preserves first seen and records only a changed price', async () => {
const raw = { vcpu_count: 1, ram: 1024, disk: 25, bandwidth: 1000, monthly_cost: 5, hourly_cost: 0.007, type: 'vc2' };
await catalog.applyItem(item('vultr-plans', 'vc2-1c-1gb', raw));