openvibers/openvibe.host Commits
Host: OVHcloud VPS plans from Sources (unmetered at their port speed), a third provider (#97)
Changes
@@@ -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 };@@@ -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) {@@@ -53,6 +53,18 @@ const item = (key, id, fields, time = at, title = id) => ({ id: `itm_${'0'.repeaconst 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));
@@ -6,6 +6,7 @@ const SOURCES = Object.freeze({
@@ -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/' },
'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/' },
'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/' },
'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) => {
const number = (v, field) => {
if (v == null) return null;
if (v == null) return null;
@@ -45,7 +46,27 @@ function lightsail(x, id) {
@@ -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
// 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.
// month): 0.00672 an hour is the $5.00 bundle AWS lists.
price_monthly: Math.round(hourly * 744 * 100) / 100, price_hourly: hourly,
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
// 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) {
@@ -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),
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 === '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 vultr = item.source_key === 'vultr-plans';
const monthly = number(vultr ? x.monthly_cost : x.price_monthly, 'monthly price');
const monthly = number(vultr ? x.monthly_cost : x.price_monthly, 'monthly price');
if (monthly == null) throw new TypeError('missing monthly price');
if (monthly == null) throw new TypeError('missing monthly price');
@@ -87,7 +112,7 @@ function mapItem(item) {
@@ -87,7 +112,7 @@ function mapItem(item) {
disk_type: vultr ? (['vhf', 'vhp'].includes(x.type) ? 'nvme' : x.type === 'vc2' ? 'ssd' : null) : null,
disk_type: vultr ? (['vhf', 'vhp'].includes(x.type) ? 'nvme' : x.type === 'vc2' ? 'ssd' : null) : null,
bandwidth_gb: number(vultr ? x.bandwidth : x.transfer, 'bandwidth'),
bandwidth_gb: number(vultr ? x.bandwidth : x.transfer, 'bandwidth'),
price_monthly: monthly, price_hourly: number(vultr ? x.hourly_cost : x.price_hourly, 'hourly price'),
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 };
module.exports = { SOURCES, mapItem };
@@ -115,7 +115,7 @@ function createCatalog({ store, community = null, log = console }) {
@@ -115,7 +115,7 @@ function createCatalog({ store, community = null, log = console }) {
p = { id };
p = { id };
}
}
const old = await t.prepare('SELECT * FROM catalog_plans WHERE provider_id = ? AND external_id = ?').get(p.id, v.external_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) {
if (old.origin !== 'sources' || v.checked_at < Number(old.checked_at)) return 'hold';
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) {
if (Number(old.price_monthly) !== v.price_monthly || old.currency !== v.currency) {
@@ -53,6 +53,18 @@ const item = (key, id, fields, time = at, title = id) => ({ id: `itm_${'0'.repea
@@ -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];
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)']]);
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 () => {
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' };
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));
await catalog.applyItem(item('vultr-plans', 'vc2-1c-1gb', raw));