CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/compute/google-cloud-config.tsx
Views: 687
1
import type {
2
Images,
3
State,
4
GoogleCloudConfiguration as GoogleCloudConfigurationType,
5
ComputeServerTemplate,
6
GoogleCloudAcceleratorType,
7
} from "@cocalc/util/db-schema/compute-servers";
8
import { reloadImages, useImages, useGoogleImages } from "./images-hook";
9
import { GOOGLE_CLOUD_DEFAULTS } from "@cocalc/util/db-schema/compute-servers";
10
import { getMinDiskSizeGb } from "@cocalc/util/db-schema/compute-servers";
11
import {
12
Alert,
13
Button,
14
Checkbox,
15
Divider,
16
Popconfirm,
17
Radio,
18
Select,
19
Spin,
20
Switch,
21
Table,
22
Tooltip,
23
Typography,
24
} from "antd";
25
import { currency, cmp, plural } from "@cocalc/util/misc";
26
import computeCost, {
27
GoogleCloudData,
28
EXTERNAL_IP_COST,
29
DATA_TRANSFER_OUT_COST_PER_GiB,
30
markup,
31
computeAcceleratorCost,
32
computeInstanceCost,
33
computeDiskCost,
34
} from "@cocalc/util/compute/cloud/google-cloud/compute-cost";
35
import {
36
getGoogleCloudPriceData,
37
setImageTested,
38
setServerConfiguration,
39
} from "./api";
40
import { useEffect, useState } from "react";
41
import { A } from "@cocalc/frontend/components/A";
42
import { Icon } from "@cocalc/frontend/components/icon";
43
import { isEqual } from "lodash";
44
import { useTypedRedux } from "@cocalc/frontend/app-framework";
45
import SelectImage, { ImageLinks, ImageDescription } from "./select-image";
46
import Ephemeral from "./ephemeral";
47
import AutoRestart from "./auto-restart";
48
import AllowCollaboratorControl from "./allow-collaborator-control";
49
import NestedVirtualization from "./nested-virtualization";
50
import ShowError from "@cocalc/frontend/components/error";
51
import Proxy from "./proxy";
52
import CostOverview from "./cost-overview";
53
import Disk from "@cocalc/frontend/compute/cloud/common/disk";
54
import DNS from "@cocalc/frontend/compute/cloud/common/dns";
55
import ExcludeFromSync from "@cocalc/frontend/compute/exclude-from-sync";
56
import { availableClouds } from "./config";
57
import Template from "@cocalc/frontend/compute/cloud/common/template";
58
import Specs, {
59
RamAndCpu,
60
} from "@cocalc/frontend/compute/cloud/google-cloud/specs";
61
import { displayAcceleratorType } from "@cocalc/frontend/compute/cloud/google-cloud/accelerator";
62
import { filterOption } from "@cocalc/frontend/compute/util";
63
64
export const SELECTOR_WIDTH = "350px";
65
66
export const DEFAULT_GPU_CONFIG = GOOGLE_CLOUD_DEFAULTS.gpu2;
67
68
// {
69
// acceleratorType: "nvidia-l4",
70
// acceleratorCount: 1,
71
// machineType: "g2-standard-4",
72
// region: "us-central1",
73
// zone: "us-central1-b",
74
// image: "pytorch",
75
// };
76
77
const FALLBACK_INSTANCE = "n2-standard-4";
78
// an n1-standard-1 is SO dinky it causes huge trouble
79
// with downloading/processing models.
80
const DEFAULT_GPU_INSTANCE = "n1-highmem-2";
81
82
interface ConfigurationType extends GoogleCloudConfigurationType {
83
valid?: boolean;
84
}
85
86
interface Props {
87
configuration: ConfigurationType;
88
editable?: boolean;
89
// if id not set, then doesn't try to save anything to the backend
90
id?: number;
91
project_id?: string;
92
// called whenever changes are made.
93
onChange?: (configuration: ConfigurationType) => void;
94
disabled?: boolean;
95
state?: State;
96
data?;
97
setCloud?;
98
template?: ComputeServerTemplate;
99
}
100
101
export default function GoogleCloudConfiguration({
102
configuration: configuration0,
103
editable,
104
id,
105
project_id,
106
onChange,
107
disabled,
108
state,
109
data,
110
setCloud,
111
template,
112
}: Props) {
113
const [IMAGES, ImagesError] = useImages();
114
const [googleImages, ImagesErrorGoogle] = useGoogleImages();
115
const [loading, setLoading] = useState<boolean>(false);
116
const [cost, setCost] = useState<number | null>(null);
117
const [priceData, setPriceData] = useState<GoogleCloudData | null>(null);
118
const [error, setError0] = useState<string>("");
119
const [configuration, setLocalConfiguration] =
120
useState<ConfigurationType>(configuration0);
121
const setError = (error) => {
122
setError0(error);
123
const valid = !error;
124
if (onChange != null && configuration.valid != valid) {
125
onChange({ ...configuration, valid });
126
}
127
};
128
129
useEffect(() => {
130
if (!editable) {
131
setLocalConfiguration(configuration0);
132
}
133
}, [configuration0]);
134
135
useEffect(() => {
136
(async () => {
137
try {
138
setLoading(true);
139
const data = await getGoogleCloudPriceData();
140
setPriceData(data);
141
} catch (err) {
142
setError(`${err}`);
143
} finally {
144
setLoading(false);
145
}
146
})();
147
}, []);
148
149
useEffect(() => {
150
if (!editable || configuration == null || priceData == null) {
151
return;
152
}
153
try {
154
const cost = computeCost({ configuration, priceData });
155
setCost(cost);
156
} catch (err) {
157
setError(`${err}`);
158
setCost(null);
159
}
160
}, [configuration, priceData]);
161
162
if (ImagesError != null) {
163
return ImagesError;
164
}
165
if (ImagesErrorGoogle != null) {
166
return ImagesErrorGoogle;
167
}
168
169
if (IMAGES == null || googleImages == null) {
170
return <Spin />;
171
}
172
173
const summary = (
174
<Specs
175
configuration={configuration}
176
priceData={priceData}
177
IMAGES={IMAGES}
178
/>
179
);
180
181
if (!editable || !project_id) {
182
// short summary only
183
return summary;
184
}
185
186
if (priceData == null) {
187
return <Spin />;
188
}
189
190
const setConfig = async (changes) => {
191
let changed = false;
192
for (const key in changes) {
193
if (!isEqual(changes[key], configuration[key])) {
194
changed = true;
195
break;
196
}
197
}
198
if (!changed) {
199
// nothing at all changed
200
return;
201
}
202
203
changes = ensureConsistentConfiguration(
204
priceData,
205
configuration,
206
changes,
207
IMAGES,
208
);
209
const newConfiguration = { ...configuration, ...changes };
210
211
if (
212
(state ?? "deprovisioned") != "deprovisioned" &&
213
(configuration.region != newConfiguration.region ||
214
configuration.zone != newConfiguration.zone)
215
) {
216
setError(
217
"Can't change the region or zone without first deprovisioning the VM",
218
);
219
// make copy so config gets reset -- i.e., whatever change you just tried to make is reverted.
220
setLocalConfiguration({ ...configuration });
221
return;
222
}
223
224
if (Object.keys(changes).length == 0) {
225
// nothing going to change
226
return;
227
}
228
229
try {
230
setLoading(true);
231
if (onChange != null) {
232
onChange(newConfiguration);
233
}
234
setLocalConfiguration(newConfiguration);
235
if (id != null) {
236
await setServerConfiguration({ id, configuration: changes });
237
}
238
} catch (err) {
239
setError(`${err}`);
240
} finally {
241
setLoading(false);
242
}
243
};
244
245
const columns = [
246
{
247
dataIndex: "value",
248
key: "value",
249
},
250
{ dataIndex: "label", key: "label", width: 130 },
251
];
252
253
const dataSource = [
254
{
255
key: "provisioning",
256
label: (
257
<A href="https://cloud.google.com/compute/docs/instances/spot">
258
<Icon name="external-link" /> Provisioning
259
</A>
260
),
261
value: (
262
<Provisioning
263
disabled={loading || disabled}
264
priceData={priceData}
265
setConfig={setConfig}
266
configuration={configuration}
267
/>
268
),
269
},
270
{
271
key: "gpu",
272
label: (
273
<A href="https://cloud.google.com/compute/docs/gpus">
274
<Icon name="external-link" /> GPUs
275
</A>
276
),
277
value: (
278
<GPU
279
state={state}
280
disabled={loading || disabled}
281
priceData={priceData}
282
setConfig={setConfig}
283
configuration={configuration}
284
IMAGES={IMAGES}
285
setCloud={setCloud}
286
/>
287
),
288
},
289
{
290
key: "image",
291
label: (
292
<ImageLinks image={configuration.image} style={{ height: "90px" }} />
293
),
294
value: (
295
<Image
296
state={state}
297
disabled={loading || disabled}
298
setConfig={setConfig}
299
configuration={configuration}
300
gpu={
301
!!(configuration.acceleratorType && configuration.acceleratorCount)
302
}
303
googleImages={googleImages}
304
arch={
305
configuration.machineType?.startsWith("t2a-") ? "arm64" : "x86_64"
306
}
307
/>
308
),
309
},
310
311
{
312
key: "machineType",
313
label: (
314
<A href="https://cloud.google.com/compute/docs/machine-resource#recommendations_for_machine_types">
315
<Icon name="external-link" /> Machine Types
316
</A>
317
),
318
value: (
319
<MachineType
320
state={state}
321
disabled={loading || disabled}
322
priceData={priceData}
323
setConfig={setConfig}
324
configuration={configuration}
325
/>
326
),
327
},
328
329
{
330
key: "region",
331
label: (
332
<A href="https://cloud.google.com/about/locations">
333
<Icon name="external-link" /> Regions
334
</A>
335
),
336
value: (
337
<Region
338
disabled={
339
loading || disabled || (state ?? "deprovisioned") != "deprovisioned"
340
}
341
priceData={priceData}
342
setConfig={setConfig}
343
configuration={configuration}
344
/>
345
),
346
},
347
{
348
key: "zone",
349
label: (
350
<A href="https://cloud.google.com/about/locations">
351
<Icon name="external-link" /> Zones
352
</A>
353
),
354
value: (
355
<Zone
356
disabled={
357
loading || disabled || (state ?? "deprovisioned") != "deprovisioned"
358
}
359
priceData={priceData}
360
setConfig={setConfig}
361
configuration={configuration}
362
/>
363
),
364
},
365
366
{
367
key: "disk",
368
label: (
369
<A href="https://cloud.google.com/compute/docs/disks/performance">
370
<Icon name="external-link" /> Disks
371
</A>
372
),
373
value: (
374
<BootDisk
375
id={id}
376
disabled={loading}
377
setConfig={setConfig}
378
configuration={configuration}
379
priceData={priceData}
380
state={state}
381
IMAGES={IMAGES}
382
/>
383
),
384
},
385
{
386
key: "exclude",
387
value: (
388
<ExcludeFromSync
389
id={id}
390
disabled={loading}
391
setConfig={setConfig}
392
configuration={configuration}
393
state={state}
394
style={{ marginTop: "10px", color: "#666" }}
395
/>
396
),
397
},
398
{
399
key: "network",
400
label: (
401
<A href="https://cloud.google.com/compute/docs/network-bandwidth">
402
<Icon name="external-link" /> Network
403
</A>
404
),
405
value: (
406
<Network
407
setConfig={setConfig}
408
configuration={configuration}
409
loading={loading}
410
priceData={priceData}
411
/>
412
),
413
},
414
{
415
key: "proxy",
416
label: <></>,
417
value: (
418
<Proxy
419
setConfig={setConfig}
420
configuration={configuration}
421
data={data}
422
state={state}
423
IMAGES={IMAGES}
424
project_id={project_id}
425
id={id}
426
/>
427
),
428
},
429
430
{
431
key: "ephemeral",
432
label: <></>,
433
value: (
434
<Ephemeral
435
setConfig={setConfig}
436
configuration={configuration}
437
loading={loading}
438
/>
439
),
440
},
441
{
442
key: "auto-restart",
443
label: <></>,
444
value: (
445
<AutoRestart
446
setConfig={setConfig}
447
configuration={configuration}
448
loading={loading}
449
/>
450
),
451
},
452
{
453
key: "allow-collaborator-control",
454
label: <></>,
455
value: (
456
<AllowCollaboratorControl
457
setConfig={setConfig}
458
configuration={configuration}
459
loading={loading}
460
/>
461
),
462
},
463
{
464
key: "nested-virtualization",
465
label: <></>,
466
value: (
467
<NestedVirtualization
468
setConfig={setConfig}
469
configuration={configuration}
470
loading={loading}
471
/>
472
),
473
},
474
{
475
key: "admin",
476
label: <></>,
477
value: (
478
<Admin
479
id={id}
480
configuration={configuration}
481
loading={loading}
482
template={template}
483
/>
484
),
485
},
486
];
487
488
const errDisplay = error ? (
489
<div
490
style={{
491
/*minHeight: "35px", */
492
padding: "5px 10px",
493
background: error ? "red" : undefined,
494
color: "white",
495
borderRadius: "5px",
496
}}
497
>
498
{error}
499
<Button
500
size="small"
501
onClick={() => {
502
setError("");
503
setLocalConfiguration(configuration0);
504
}}
505
style={{ float: "right" }}
506
>
507
Close
508
</Button>
509
</div>
510
) : undefined;
511
512
return (
513
<div>
514
{loading && (
515
<div style={{ float: "right" }}>
516
<Spin delay={1000} />
517
</div>
518
)}
519
{errDisplay}
520
{cost != null && (
521
<CostOverview
522
cost={cost}
523
description={
524
<>
525
You pay <b>{currency(cost)}/hour</b> while the computer server is
526
running. The rate is{" "}
527
<b>
528
{currency(
529
computeCost({ configuration, priceData, state: "off" }),
530
)}
531
/hour
532
</b>{" "}
533
when the server is off, and there is no cost when it is
534
deprovisioned. Network data transfer out charges are not included
535
in the above cost, and depend on how much data leaves the server
536
(see the Network section below). Incoming networking is free.
537
</>
538
}
539
/>
540
)}
541
<Divider />
542
<div style={{ textAlign: "center", margin: "10px 80px" }}>{summary}</div>
543
<Divider />
544
<Table
545
showHeader={false}
546
style={{ marginTop: "5px" }}
547
columns={columns}
548
dataSource={dataSource}
549
pagination={false}
550
/>
551
{errDisplay}
552
</div>
553
);
554
}
555
556
function Region({ priceData, setConfig, configuration, disabled }) {
557
const [sortByPrice, setSortByPrice] = useState<boolean>(true);
558
const [newRegion, setNewRegion] = useState<string>(configuration.region);
559
useEffect(() => {
560
setNewRegion(configuration.region);
561
}, [configuration.region]);
562
563
const regions = getRegions(priceData, configuration);
564
if (sortByPrice) {
565
regions.sort((a, b) => cmp(a.cost, b.cost));
566
}
567
const options = regions.map(({ region, location, lowCO2, cost }) => {
568
const price = <CostPerHour cost={cost} extra={" (total)"} />;
569
return {
570
value: region,
571
search: `${region} ${location} ${lowCO2 ? " co2 " : ""}`,
572
label: (
573
<div key={region} style={{ display: "flex" }}>
574
<div style={{ flex: 1 }}> {region}</div>
575
<div style={{ flex: 1 }}>{price}</div>
576
<div style={{ flex: 0.7 }}> {lowCO2 ? "🍃 Low CO2" : ""}</div>
577
<div style={{ flex: 0.8 }}> {location?.split(",")[1].trim()}</div>
578
</div>
579
),
580
};
581
});
582
583
return (
584
<div>
585
{configuration.machineType ? (
586
<div style={{ color: "#666", marginBottom: "5px" }}>
587
<b>
588
<Icon name="global" /> Region
589
</b>
590
</div>
591
) : undefined}
592
<div>
593
<Select
594
disabled={disabled}
595
style={{ width: "100%" }}
596
options={options as any}
597
value={newRegion}
598
onChange={(region) => {
599
setNewRegion(region);
600
setConfig({ region });
601
}}
602
showSearch
603
optionFilterProp="children"
604
filterOption={filterOption}
605
/>
606
</div>
607
<div>
608
<Checkbox
609
disabled={disabled}
610
style={{ marginTop: "5px" }}
611
checked={sortByPrice}
612
onChange={() => setSortByPrice(!sortByPrice)}
613
>
614
Sort by price
615
</Checkbox>
616
<div style={{ color: "#666", marginTop: "5px" }}>
617
Price above is total price in this region for the machine, disk and
618
GPU.
619
</div>
620
</div>
621
</div>
622
);
623
}
624
625
// Gets the regions where the given VM type is available.
626
// Ignores the currently selected zone.
627
function getRegions(priceData, configuration) {
628
const lowCO2 = new Set<string>();
629
const regions = new Set<string>();
630
const location: { [region: string]: string } = {};
631
const cost: { [region: string]: number } = {};
632
const { machineType, spot } = configuration ?? {};
633
for (const zone in priceData.zones) {
634
const i = zone.lastIndexOf("-");
635
const region = zone.slice(0, i);
636
const zoneData = priceData.zones[zone];
637
if (machineType) {
638
if (!zoneData.machineTypes.includes(machineType.split("-")[0])) {
639
continue;
640
}
641
if (spot) {
642
if (priceData.machineTypes[machineType]?.spot?.[region] == null) {
643
continue;
644
}
645
}
646
}
647
if (cost[region] == null) {
648
try {
649
cost[region] = computeCost({
650
priceData,
651
configuration: { ...configuration, region, zone },
652
});
653
} catch (_) {
654
continue;
655
// console.warn({ ...configuration, region, zone }, err);
656
}
657
}
658
if (zoneData.lowCO2 || zoneData.lowC02) {
659
// C02 above because of typo in data.
660
lowCO2.add(region);
661
}
662
regions.add(region);
663
location[region] = zoneData.location;
664
}
665
const v = Array.from(regions);
666
v.sort((a, b) => {
667
for (const g of [
668
"us",
669
"northamerica",
670
"europe",
671
"asia",
672
"southamerica",
673
"australia",
674
]) {
675
if (a.startsWith(g) && !b.startsWith(g)) {
676
return -1;
677
}
678
if (!a.startsWith(g) && b.startsWith(g)) {
679
return 1;
680
}
681
}
682
return cmp(a, b);
683
});
684
const data: {
685
region: string;
686
location: string;
687
lowCO2: boolean;
688
cost?: number;
689
}[] = [];
690
for (const region of v) {
691
data.push({
692
region,
693
location: location[region],
694
lowCO2: lowCO2.has(region),
695
cost: cost[region],
696
});
697
}
698
return data;
699
}
700
701
// Gets the zones compatible with the other configuration
702
function getZones(priceData, configuration) {
703
const lowCO2 = new Set<string>();
704
const zones = new Set<string>();
705
const { region, machineType, acceleratorType, spot } = configuration;
706
const prefix = machineType.split("-")[0];
707
for (const zone in priceData.zones) {
708
if (region != zoneToRegion(zone)) {
709
// this zone isn't in the chosen region.
710
continue;
711
}
712
const zoneData = priceData.zones[zone];
713
if (machineType) {
714
if (!zoneData.machineTypes.includes(prefix)) {
715
continue;
716
}
717
if (spot != null) {
718
if (priceData.machineTypes[machineType]?.spot?.[region] == null) {
719
continue;
720
}
721
}
722
}
723
if (acceleratorType) {
724
if (priceData.accelerators[acceleratorType]?.prices?.[zone] == null) {
725
// not in this zone.
726
continue;
727
}
728
}
729
if (zoneData.lowCO2 || zoneData.lowC02) {
730
// C02 above because of typo in data.
731
lowCO2.add(zone);
732
}
733
zones.add(zone);
734
}
735
const v = Array.from(zones);
736
v.sort();
737
const data: {
738
zone: string;
739
lowCO2: boolean;
740
}[] = [];
741
for (const zone of v) {
742
data.push({
743
zone,
744
lowCO2: lowCO2.has(zone),
745
});
746
}
747
return data;
748
}
749
750
function Provisioning({ priceData, setConfig, configuration, disabled }) {
751
const [newSpot, setNewSpot] = useState<boolean>(!!configuration.spot);
752
const [prices, setPrices] = useState<{
753
spot: number | null;
754
standard: number;
755
discount: number;
756
} | null>(getSpotAndStandardPrices(priceData, configuration));
757
758
useEffect(() => {
759
setNewSpot(!!configuration.spot);
760
setPrices(getSpotAndStandardPrices(priceData, configuration));
761
}, [configuration]);
762
763
useEffect(() => {
764
if (configuration.spot && prices != null && !prices.spot) {
765
setNewSpot(false);
766
setConfig({ spot: false });
767
}
768
}, [prices, configuration.spot]);
769
770
return (
771
<div>
772
<div style={{ color: "#666", marginBottom: "5px" }}>
773
<b>
774
<Icon name="sliders" /> Provisioning
775
</b>
776
</div>
777
<Radio.Group
778
size="large"
779
buttonStyle="solid"
780
disabled={disabled}
781
value={newSpot ? "spot" : "standard"}
782
onChange={(e) => {
783
const spot = e.target.value == "standard" ? false : true;
784
setNewSpot(spot);
785
setConfig({ spot });
786
}}
787
>
788
<Radio.Button value="spot" disabled={!prices?.spot}>
789
Spot{" "}
790
{prices?.spot
791
? `${currency(prices.spot)}/hour (${prices.discount}% discount)`
792
: "(not available)"}{" "}
793
</Radio.Button>
794
<Radio.Button value="standard">
795
Standard{" "}
796
{prices != null ? `${currency(prices.standard)}/hour` : undefined}{" "}
797
</Radio.Button>
798
</Radio.Group>
799
<div style={{ color: "#666", marginTop: "5px" }}>
800
Standard VM's run until you stop them, whereas spot VM's are up to 91%
801
off, but will automatically stop when there is a surge in demand. Spot
802
instances might also not be available in a given region, so you may have
803
to try different regions.{" "}
804
{configuration.acceleratorType && (
805
<> GPU's are always in high demand.</>
806
)}
807
{newSpot && (
808
<Alert
809
style={{ margin: "5px 0" }}
810
type="warning"
811
showIcon
812
description={
813
<div style={{ maxWidth: "100%", lineHeight: 1 }}>
814
This is a heavily discounted spot instance. It will
815
automatically{" "}
816
{configuration.autoRestart ? " reboot if possible " : " stop "}{" "}
817
when there is a surge in demand.
818
{!disabled && (
819
<Popconfirm
820
title="Switch to Standard?"
821
description={
822
<div style={{ maxWidth: "450px" }}>
823
This will switch to a non-discounted standard instance,
824
which stays running even if there is high demand. You
825
can switch back to a spot instance using the blue toggle
826
above.
827
</div>
828
}
829
onConfirm={() => {
830
setNewSpot(false);
831
setConfig({ spot: false });
832
}}
833
okText="Switch to Standard"
834
cancelText="Cancel"
835
>
836
<Button type="link">Switch to Standard</Button>
837
</Popconfirm>
838
)}
839
{!configuration.autoRestart && (
840
<Popconfirm
841
title="Enable Automatic Restart?"
842
description={
843
<div style={{ maxWidth: "450px" }}>
844
CoCalc will automatically restart your compute server if
845
it is killed due to high demand. Note that there might
846
not be any compute resources available, in which case
847
you will have to wait for your server to start. You can
848
disable this in the "Automatically Restart" section
849
below.
850
</div>
851
}
852
onConfirm={() => {
853
setConfig({ autoRestart: true });
854
}}
855
okText="Enable Automatic Restart"
856
cancelText="Cancel"
857
>
858
<Button type="link">Enable Automatic Restart</Button>
859
</Popconfirm>
860
)}
861
</div>
862
}
863
/>
864
)}
865
</div>
866
</div>
867
);
868
}
869
870
function getSpotAndStandardPrices(priceData, configuration) {
871
try {
872
const standard = computeCost({
873
priceData,
874
configuration: { ...configuration, spot: false },
875
});
876
let spot: number | null = null;
877
try {
878
spot = computeCost({
879
priceData,
880
configuration: { ...configuration, spot: true },
881
});
882
} catch (_) {
883
// some machines have no spot instance support, eg h3's.
884
}
885
return {
886
standard,
887
spot,
888
discount: spot != null ? Math.round((1 - spot / standard) * 100) : 0,
889
};
890
} catch (_) {
891
return null;
892
}
893
}
894
895
function Zone({ priceData, setConfig, configuration, disabled }) {
896
const [newZone, setNewZone] = useState<string>(configuration.zone ?? "");
897
useEffect(() => {
898
setNewZone(configuration.zone);
899
}, [configuration.zone]);
900
901
const zones = getZones(priceData, configuration);
902
const options = zones.map(({ zone, lowCO2 }) => {
903
return {
904
value: zone,
905
search: `${zone} ${lowCO2 ? " co 2" : ""}`,
906
label: `${zone} ${lowCO2 ? " - 🍃 Low CO2" : ""}`,
907
};
908
});
909
910
return (
911
<div>
912
{configuration.machineType ? (
913
<div style={{ color: "#666", marginBottom: "5px" }}>
914
<b>
915
<Icon name="aim" /> Zone
916
</b>{" "}
917
in {configuration.region} with {configuration.machineType}{" "}
918
{configuration.spot ? "spot" : ""} VM's
919
</div>
920
) : undefined}
921
<Select
922
disabled={disabled}
923
style={{ width: SELECTOR_WIDTH }}
924
options={options}
925
value={newZone}
926
onChange={(zone) => {
927
setNewZone(zone);
928
setConfig({ zone });
929
}}
930
showSearch
931
optionFilterProp="children"
932
filterOption={filterOption}
933
/>
934
</div>
935
);
936
}
937
938
function MachineType({ priceData, setConfig, configuration, disabled, state }) {
939
const [archType, setArchType] = useState<"x86_64" | "arm64">(
940
configuration.machineType?.startsWith("t2a-") ? "arm64" : "x86_64",
941
);
942
const [sortByPrice, setSortByPrice] = useState<boolean>(true);
943
const [newMachineType, setNewMachineType] = useState<string>(
944
configuration.machineType ?? "",
945
);
946
useEffect(() => {
947
setNewMachineType(configuration.machineType);
948
setArchType(
949
configuration.machineType?.startsWith("t2a-") ? "arm64" : "x86_64",
950
);
951
}, [configuration.machineType]);
952
useEffect(() => {
953
if (archType == "arm64" && !configuration.machineType.startsWith("t2a-")) {
954
setNewMachineType("t2a-standard-4");
955
setConfig({ machineType: "t2a-standard-4" });
956
return;
957
}
958
if (archType == "x86_64" && configuration.machineType.startsWith("t2a-")) {
959
setNewMachineType("t2d-standard-4");
960
setConfig({ machineType: "t2d-standard-4" });
961
return;
962
}
963
}, [archType, configuration.machineType]);
964
965
const machineTypes = Object.keys(priceData.machineTypes);
966
let allOptions = machineTypes
967
.filter((machineType) => {
968
const { acceleratorType } = configuration;
969
if (!acceleratorType) {
970
if (machineType.startsWith("g2-") || machineType.startsWith("a2-")) {
971
return false;
972
}
973
if (archType == "arm64" && !machineType.startsWith("t2a-")) {
974
return false;
975
}
976
if (archType == "x86_64" && machineType.startsWith("t2a-")) {
977
return false;
978
}
979
} else {
980
if (
981
acceleratorType == "nvidia-tesla-a100" ||
982
acceleratorType == "nvidia-a100-80gb" ||
983
acceleratorType == "nvidia-l4"
984
) {
985
const machines =
986
priceData.accelerators[acceleratorType].machineType[
987
configuration.acceleratorCount ?? 1
988
] ?? [];
989
return machines.includes(machineType);
990
} else {
991
return machineType.startsWith("n1-");
992
}
993
}
994
995
return true;
996
})
997
.map((machineType) => {
998
let cost;
999
try {
1000
cost = computeInstanceCost({
1001
priceData,
1002
configuration: { ...configuration, machineType },
1003
});
1004
} catch (_) {
1005
cost = null;
1006
}
1007
const data = priceData.machineTypes[machineType];
1008
const { memory, vcpu } = data;
1009
return {
1010
value: machineType,
1011
search: machineType + ` memory:${memory} ram:${memory} cpu:${vcpu} `,
1012
cost,
1013
label: (
1014
<div key={machineType} style={{ display: "flex" }}>
1015
<div style={{ flex: 1 }}>{machineType}</div>
1016
<div style={{ flex: 1 }}>
1017
{cost ? (
1018
<CostPerHour cost={cost} />
1019
) : (
1020
<span style={{ color: "#666" }}>(region/zone changes)</span>
1021
)}
1022
</div>
1023
<div style={{ flex: 2 }}>
1024
<RamAndCpu machineType={machineType} priceData={priceData} />
1025
</div>
1026
</div>
1027
),
1028
};
1029
});
1030
const options = [
1031
{
1032
label: "Machine Types",
1033
options: allOptions.filter((x) => x.cost),
1034
},
1035
{
1036
label: "Location Will Change",
1037
options: allOptions.filter((x) => !x.cost),
1038
},
1039
];
1040
1041
if (sortByPrice) {
1042
options[0].options.sort((a, b) => {
1043
return cmp(a.cost, b.cost);
1044
});
1045
}
1046
1047
return (
1048
<div>
1049
<div style={{ color: "#666", marginBottom: "5px" }}>
1050
<Tooltip
1051
title={
1052
(state ?? "deprovisioned") != "deprovisioned"
1053
? "Can only be changed when machine is deprovisioned"
1054
: archType == "x86_64"
1055
? "Intel or AMD X86_64 architecture machines"
1056
: "ARM64 architecture machines"
1057
}
1058
>
1059
<Radio.Group
1060
style={{ float: "right" }}
1061
disabled={
1062
disabled ||
1063
configuration.acceleratorType ||
1064
(state ?? "deprovisioned") != "deprovisioned"
1065
}
1066
options={[
1067
{ value: "x86_64", label: "X86_64" },
1068
{ value: "arm64", label: "ARM64" },
1069
]}
1070
value={archType}
1071
onChange={({ target: { value } }) => {
1072
setArchType(value);
1073
}}
1074
/>
1075
</Tooltip>
1076
<b>
1077
<Icon name="microchip" /> Machine Type
1078
</b>
1079
</div>
1080
<div>
1081
<Select
1082
disabled={disabled}
1083
style={{ width: "100%" }}
1084
options={options as any}
1085
value={newMachineType}
1086
onChange={(machineType) => {
1087
setNewMachineType(machineType);
1088
setConfig({ machineType });
1089
}}
1090
showSearch
1091
optionFilterProp="children"
1092
filterOption={filterOption}
1093
/>
1094
</div>
1095
<div>
1096
<Checkbox
1097
disabled={disabled}
1098
style={{ marginTop: "5px" }}
1099
checked={sortByPrice}
1100
onChange={() => setSortByPrice(!sortByPrice)}
1101
>
1102
Sort by price
1103
</Checkbox>
1104
</div>
1105
<div style={{ color: "#666", marginTop: "5px" }}>
1106
Prices and availability depend on the region and provisioning type, so
1107
adjust those below to find the best overall value. Price above is just
1108
for the machine, and not the disk or GPU. Search for <code>cpu:4⌴</code>{" "}
1109
and <code>ram:8⌴</code> to only show options with 4 vCPUs and 8GB RAM.
1110
</div>
1111
</div>
1112
);
1113
}
1114
1115
function BootDisk(props) {
1116
return (
1117
<Disk
1118
{...props}
1119
minSizeGb={getMinDiskSizeGb(props)}
1120
maxSizeGb={65536}
1121
computeDiskCost={computeDiskCost}
1122
/>
1123
);
1124
}
1125
1126
function Image(props) {
1127
const { state = "deprovisioned" } = props;
1128
return (
1129
<div>
1130
<div style={{ color: "#666", marginBottom: "5px" }}>
1131
<b>
1132
<Icon name="disk-round" /> Image
1133
</b>
1134
</div>
1135
{state == "deprovisioned" && (
1136
<div style={{ color: "#666", marginBottom: "5px" }}>
1137
Select compute server image. You will be able to use sudo as root with
1138
no password, and can install anything into the Ubuntu Linux image,
1139
including commercial software.
1140
</div>
1141
)}
1142
<SelectImage {...props} />
1143
{state != "deprovisioned" && (
1144
<div style={{ color: "#666", marginTop: "5px" }}>
1145
You can only edit the image when server is deprovisioned.
1146
</div>
1147
)}
1148
<div style={{ color: "#666", marginTop: "5px" }}>
1149
<ImageDescription configuration={props.configuration} />
1150
</div>
1151
</div>
1152
);
1153
}
1154
1155
// We do NOT include the P4, P100, V100 or K80, which are older
1156
// and for which our base image and drivers don't work.
1157
// If for some reason we need them, we will have to switch to
1158
// different base drivers or have even more images
1159
const ACCELERATOR_TYPES = [
1160
"nvidia-tesla-t4",
1161
"nvidia-l4",
1162
"nvidia-tesla-a100",
1163
"nvidia-a100-80gb",
1164
// "nvidia-tesla-v100",
1165
//"nvidia-tesla-p100",
1166
//"nvidia-tesla-p4",
1167
];
1168
1169
/*
1170
<A href="https://www.nvidia.com/en-us/data-center/tesla-p100/">P100</A>,{" "}
1171
<A href="https://www.nvidia.com/en-us/data-center/v100/">V100</A>,{" "}
1172
<A href="https://www.nvidia.com/content/dam/en-zz/Solutions/design-visualization/solutions/resources/documents1/nvidia-p4-datasheet.pdf">
1173
P4
1174
</A>
1175
*/
1176
1177
function GPU({
1178
priceData,
1179
setConfig,
1180
configuration,
1181
disabled,
1182
state,
1183
IMAGES,
1184
setCloud,
1185
}) {
1186
const { acceleratorType, acceleratorCount } = configuration;
1187
const head = (
1188
<div style={{ color: "#666", marginBottom: "5px" }}>
1189
<b>
1190
<Icon style={{ float: "right", fontSize: "50px" }} name="gpu" />
1191
<Icon name="cube" /> NVIDIA GPU:{" "}
1192
<A href="https://www.nvidia.com/en-us/data-center/a100/">A100</A>,{" "}
1193
<A href="https://www.nvidia.com/en-us/data-center/l4/">L4</A>,{" "}
1194
<A href="https://www.nvidia.com/content/dam/en-zz/Solutions/design-visualization/solutions/resources/documents1/Datasheet_NVIDIA_T4_Virtualization.pdf">
1195
T4
1196
</A>
1197
</b>
1198
</div>
1199
);
1200
1201
const theSwitch = (
1202
<Switch
1203
disabled={disabled || (state ?? "deprovisioned") != "deprovisioned"}
1204
checkedChildren={"NVIDIA GPU"}
1205
unCheckedChildren={"NO GPU"}
1206
checked={!!acceleratorType}
1207
onChange={() => {
1208
if (!!acceleratorType) {
1209
setConfig({ acceleratorType: "", acceleratorCount: 0 });
1210
} else {
1211
setConfig(DEFAULT_GPU_CONFIG);
1212
}
1213
}}
1214
/>
1215
);
1216
if (!acceleratorType) {
1217
return (
1218
<div>
1219
{head}
1220
{theSwitch}
1221
</div>
1222
);
1223
}
1224
1225
const options = ACCELERATOR_TYPES.filter(
1226
(acceleratorType) => priceData.accelerators[acceleratorType] != null,
1227
).map((acceleratorType: GoogleCloudAcceleratorType) => {
1228
let cost;
1229
const config1 = { ...configuration, acceleratorType, acceleratorCount };
1230
const changes = { acceleratorType, acceleratorCount };
1231
try {
1232
cost = computeAcceleratorCost({ priceData, configuration: config1 });
1233
} catch (_) {
1234
const newChanges = ensureConsistentConfiguration(
1235
priceData,
1236
config1,
1237
changes,
1238
IMAGES,
1239
);
1240
cost = computeAcceleratorCost({
1241
priceData,
1242
configuration: { ...config1, ...newChanges },
1243
});
1244
}
1245
const memory = priceData.accelerators[acceleratorType].memory;
1246
return {
1247
value: acceleratorType,
1248
search: acceleratorType,
1249
cost,
1250
memory,
1251
label: (
1252
<div key={acceleratorType} style={{ display: "flex" }}>
1253
<div style={{ flex: 1 }}>
1254
{displayAcceleratorType(acceleratorType, memory)}
1255
</div>
1256
<div style={{ flex: 1 }}>
1257
<CostPerHour cost={cost} />
1258
</div>
1259
</div>
1260
),
1261
};
1262
});
1263
1264
const countOptions: any[] = [];
1265
const min = priceData.accelerators[acceleratorType]?.count ?? 1;
1266
const max = priceData.accelerators[acceleratorType]?.max ?? 1;
1267
for (let i = min; i <= max; i *= 2) {
1268
countOptions.push({ label: `${i}`, value: i });
1269
}
1270
1271
return (
1272
<div>
1273
{head}
1274
{theSwitch}
1275
<div style={{ marginTop: "15px" }}>
1276
<Select
1277
disabled={disabled || (state ?? "deprovisioned") != "deprovisioned"}
1278
style={{ width: SELECTOR_WIDTH }}
1279
options={options as any}
1280
value={acceleratorType}
1281
onChange={(type) => {
1282
setConfig({ acceleratorType: type });
1283
// todo -- change count if necessary
1284
}}
1285
showSearch
1286
optionFilterProp="children"
1287
filterOption={filterOption}
1288
/>
1289
<Select
1290
style={{ marginLeft: "15px", width: "75px" }}
1291
disabled={disabled || (state ?? "deprovisioned") != "deprovisioned"}
1292
options={countOptions}
1293
value={acceleratorCount}
1294
onChange={(count) => {
1295
setConfig({ acceleratorCount: count });
1296
}}
1297
/>
1298
{acceleratorCount && acceleratorType && (
1299
<div style={{ color: "#666", marginTop: "10px" }}>
1300
You have selected {acceleratorCount} dedicated{" "}
1301
<b>{displayAcceleratorType(acceleratorType)}</b>{" "}
1302
{plural(acceleratorCount, "GPU")}, with a total of{" "}
1303
<b>
1304
{priceData.accelerators[acceleratorType].memory *
1305
acceleratorCount}
1306
GB GPU RAM
1307
</b>
1308
.{" "}
1309
{acceleratorCount > 1 && (
1310
<>
1311
The {acceleratorCount} GPUs will be available on the same
1312
server.
1313
</>
1314
)}
1315
{
1316
(state ?? "deprovisioned") != "deprovisioned" && (
1317
<div>
1318
You can only change the GPU configuration when the server is
1319
deprovisioned.
1320
</div>
1321
) /* this is mostly a google limitation, not cocalc, though we will eventually do somthing involving recreating the machine. BUT note that e.g., changing the count for L4's actually breaks booting up! */
1322
}
1323
{setCloud != null &&
1324
availableClouds().includes("hyperstack") &&
1325
(state ?? "deprovisioned") == "deprovisioned" && (
1326
<Alert
1327
showIcon
1328
style={{ margin: "10px 0 5px 0" }}
1329
type="warning"
1330
description={
1331
<div>
1332
We have partnered with Hyperstack cloud to provide NVIDIA
1333
H100, A100, L40, and RTX-A4/5/6000 GPUs at a{" "}
1334
<b>much cheaper price</b> than Google cloud.{" "}
1335
<Popconfirm
1336
title="Switch to Hyperstack"
1337
description={
1338
<div style={{ maxWidth: "450px" }}>
1339
This will change the cloud for this compute server
1340
to Hyperstack, and reset its configuration. Your
1341
compute server is not storing any data so this is
1342
safe.
1343
</div>
1344
}
1345
onConfirm={() => {
1346
setCloud("hyperstack");
1347
}}
1348
okText="Switch to Hyperstack"
1349
cancelText="Cancel"
1350
>
1351
<Button type="link">Switch...</Button>
1352
</Popconfirm>
1353
</div>
1354
}
1355
/>
1356
)}
1357
</div>
1358
)}
1359
</div>
1360
</div>
1361
);
1362
}
1363
/*
1364
{acceleratorType?.includes("a100") && configuration.spot ? (
1365
<div style={{ marginTop: "5px", color: "#666" }}>
1366
<b>WARNING:</b> A100 spot instances are rarely available. Consider
1367
standard provisioning instead.
1368
</div>
1369
) : undefined}
1370
*/
1371
1372
function ensureConsistentConfiguration(
1373
priceData,
1374
configuration: GoogleCloudConfigurationType,
1375
changes: Partial<GoogleCloudConfigurationType>,
1376
IMAGES: Images,
1377
) {
1378
const newConfiguration = { ...configuration, ...changes };
1379
const newChanges = { ...changes };
1380
1381
ensureConsistentImage(newConfiguration, newChanges, IMAGES);
1382
1383
ensureConsistentAccelerator(priceData, newConfiguration, newChanges);
1384
1385
ensureConsistentNvidiaL4andA100(priceData, newConfiguration, newChanges);
1386
1387
ensureConsistentRegionAndZoneWithMachineType(
1388
priceData,
1389
newConfiguration,
1390
newChanges,
1391
);
1392
1393
ensureConsistentZoneWithRegion(priceData, newConfiguration, newChanges);
1394
1395
ensureSufficientDiskSize(newConfiguration, newChanges, IMAGES);
1396
1397
ensureConsistentDiskType(priceData, newConfiguration, newChanges);
1398
1399
return newChanges;
1400
}
1401
1402
// We make the image consistent with the gpu selection.
1403
function ensureConsistentImage(configuration, changes, IMAGES) {
1404
const { gpu } = IMAGES[configuration.image] ?? {};
1405
const gpuSelected =
1406
configuration.acceleratorType && configuration.acceleratorCount > 0;
1407
if (gpu == gpuSelected) {
1408
// they are consistent
1409
return;
1410
}
1411
if (gpu && !gpuSelected) {
1412
// GPU image but non-GPU machine -- change image to non-GPU
1413
configuration["image"] = changes["image"] = "python";
1414
configuration["tag"] = changes["tag"] = null;
1415
} else if (!gpu && gpuSelected) {
1416
// GPU machine but not image -- change image to pytorch
1417
configuration["image"] = changes["image"] = "pytorch";
1418
configuration["tag"] = changes["tag"] = null;
1419
}
1420
}
1421
1422
function ensureSufficientDiskSize(configuration, changes, IMAGES) {
1423
const min = getMinDiskSizeGb({ configuration, IMAGES });
1424
if ((configuration.diskSizeGb ?? 0) < min) {
1425
changes.diskSizeGb = min;
1426
}
1427
}
1428
1429
function ensureConsistentDiskType(priceData, configuration, changes) {
1430
const { machineType } = configuration;
1431
const m = machineType.split("-")[0];
1432
if (configuration.diskType == "hyperdisk-balanced") {
1433
// make sure machine is supported
1434
const { supportedMachineTypes } = priceData.extra["hyperdisk-balanced"];
1435
if (!supportedMachineTypes.includes(m)) {
1436
// can't use hyperdisk on this machine, so fix.
1437
configuration.diskType = changes.diskType = "pd-balanced";
1438
}
1439
} else {
1440
const { requiredMachineTypes } = priceData.extra["hyperdisk-balanced"];
1441
if (requiredMachineTypes.includes(m)) {
1442
// must use hyperdisk on this machine, so fix.
1443
configuration.diskType = changes.diskType = "hyperdisk-balanced";
1444
}
1445
}
1446
}
1447
1448
function ensureConsistentZoneWithRegion(priceData, configuration, changes) {
1449
if (configuration.zone.startsWith(configuration.region)) {
1450
return;
1451
}
1452
if (changes["region"]) {
1453
// currently changing region, so set a zone that matches the region
1454
for (const zone in priceData.zones) {
1455
if (zone.startsWith(configuration.region)) {
1456
changes["zone"] = zone;
1457
break;
1458
}
1459
}
1460
} else {
1461
// probably changing the zone, so set the region from the zone
1462
changes["region"] = zoneToRegion(configuration.zone);
1463
}
1464
}
1465
1466
function ensureConsistentAccelerator(priceData, configuration, changes) {
1467
let { acceleratorType } = configuration;
1468
if (!acceleratorType) {
1469
return;
1470
}
1471
if (
1472
acceleratorType == "nvidia-tesla-a100" ||
1473
acceleratorType == "nvidia-a100-80gb" ||
1474
acceleratorType == "nvidia-l4"
1475
) {
1476
// L4 and A100 are handled elsewhere.
1477
return;
1478
}
1479
1480
// have a GPU
1481
let data = priceData.accelerators[acceleratorType];
1482
if (!data) {
1483
// accelerator type no longer exists; replace it by one that does.
1484
for (const type in priceData.accelerators) {
1485
acceleratorType =
1486
configuration["acceleratorType"] =
1487
changes["acceleratorType"] =
1488
type;
1489
data = priceData.accelerators[acceleratorType];
1490
break;
1491
}
1492
}
1493
if (data == null) {
1494
throw Error("bug");
1495
}
1496
// Ensure the machine type is consistent
1497
if (!configuration.machineType.startsWith(data.machineType)) {
1498
if (changes["machineType"]) {
1499
// if you are explicitly changing the machine type, then we respect
1500
// that and disabled the gpu
1501
configuration["acceleratorType"] = changes["acceleratorType"] = "";
1502
configuration["acceleratorCount"] = changes["acceleratorCount"] = 0;
1503
return;
1504
} else {
1505
// changing something else, so we fix the machine type
1506
for (const type in priceData.machineTypes) {
1507
if (type.startsWith(data.machineType)) {
1508
configuration["machineType"] = changes["machineType"] =
1509
type.startsWith("n1-") ? DEFAULT_GPU_INSTANCE : type;
1510
break;
1511
}
1512
}
1513
}
1514
}
1515
ensureZoneIsConsistentWithGPU(priceData, configuration, changes);
1516
1517
// Ensure the count is consistent
1518
const count = configuration.acceleratorCount ?? 0;
1519
if (count < data.count) {
1520
changes["acceleratorCount"] = data.count;
1521
} else if (count > data.max) {
1522
changes["acceleratorCount"] = data.max;
1523
}
1524
}
1525
1526
function ensureZoneIsConsistentWithGPU(priceData, configuration, changes) {
1527
if (!configuration.acceleratorType) return;
1528
1529
const data = priceData.accelerators[configuration.acceleratorType];
1530
if (!data) {
1531
// invalid acceleratorType.
1532
return;
1533
}
1534
1535
// Ensure the region/zone is consistent with accelerator type
1536
const prices = data[configuration.spot ? "spot" : "prices"];
1537
if (prices[configuration.zone] == null) {
1538
// there are no GPUs in the selected zone of the selected type.
1539
// If you just explicitly changed the GPU type, then we fix this by changing the zone.
1540
if (changes["acceleratorType"] != null) {
1541
// fix the region and zone
1542
// find cheapest zone in the world.
1543
let price = 999999999;
1544
let zoneChoice = "";
1545
for (const zone in prices) {
1546
if (prices[zone] < price) {
1547
price = prices[zone];
1548
zoneChoice = zone;
1549
}
1550
}
1551
if (zoneChoice) {
1552
changes["zone"] = configuration["zone"] = zoneChoice;
1553
changes["region"] = configuration["region"] = zoneToRegion(zoneChoice);
1554
return;
1555
}
1556
} else {
1557
// You did not change the GPU type, so we disable the GPU
1558
configuration["acceleratorType"] = changes["acceleratorType"] = "";
1559
configuration["acceleratorCount"] = changes["acceleratorCount"] = 0;
1560
return;
1561
}
1562
}
1563
}
1564
1565
// The Nvidia L4 and A100 are a little different
1566
function ensureConsistentNvidiaL4andA100(priceData, configuration, changes) {
1567
const { machineType, acceleratorType } = configuration;
1568
1569
// L4 or A100 GPU machine type, but switching to no GPU, so we have
1570
// to change the machine type
1571
if (machineType.startsWith("g2-") || machineType.startsWith("a2-")) {
1572
if (!acceleratorType) {
1573
// Easy case -- the user is explicitly changing the GPU from being set
1574
// to NOT be set, and the GPU is L4 or A100. In this case,
1575
// we just set the machine type to some non-gpu type
1576
// and we're done.
1577
configuration.machineType = changes.machineType = FALLBACK_INSTANCE;
1578
return;
1579
}
1580
}
1581
if (
1582
acceleratorType != "nvidia-tesla-a100" &&
1583
acceleratorType != "nvidia-a100-80gb" &&
1584
acceleratorType != "nvidia-l4"
1585
) {
1586
// We're not switching to an A100 or L4, so not handled further here.
1587
return;
1588
}
1589
1590
if (!configuration.acceleratorCount) {
1591
configuration.acceleratorCount = changes.acceleratorCount = 1;
1592
}
1593
1594
// Ensure machine type is consistent with the GPU and count we're switching to.
1595
let machineTypes =
1596
priceData.accelerators[acceleratorType]?.machineType[
1597
configuration.acceleratorCount
1598
];
1599
if (machineTypes == null) {
1600
configuration.acceleratorCount = changes.acceleratorCount = 1;
1601
machineTypes =
1602
priceData.accelerators[acceleratorType]?.machineType[
1603
configuration.acceleratorCount
1604
];
1605
}
1606
if (machineTypes == null) {
1607
throw Error("bug -- this can't happen");
1608
}
1609
1610
if (!machineTypes.includes(configuration.machineType)) {
1611
configuration.machineType = changes.machineType =
1612
machineTypes[0].startsWith("n1-")
1613
? DEFAULT_GPU_INSTANCE
1614
: machineTypes[0];
1615
}
1616
}
1617
1618
function ensureConsistentRegionAndZoneWithMachineType(
1619
priceData,
1620
configuration,
1621
changes,
1622
) {
1623
// Specifically selecting a machine type. We make this the
1624
// highest priority, so if you are changing this, we make everything
1625
// else fit it.
1626
const machineType = configuration["machineType"];
1627
if (priceData.machineTypes[machineType] == null) {
1628
console.warn(
1629
`BUG -- This should never happen: unknonwn machineType = '${machineType}'`,
1630
);
1631
// invalid machineType
1632
if (configuration.acceleratorType) {
1633
configuration["machineType"] = changes["machineType"] =
1634
DEFAULT_GPU_INSTANCE;
1635
} else {
1636
configuration["machineType"] = changes["machineType"] = FALLBACK_INSTANCE;
1637
}
1638
return;
1639
}
1640
1641
const i = machineType.indexOf("-");
1642
const prefix = machineType.slice(0, i);
1643
1644
let zoneHasMachineType = (
1645
priceData.zones[configuration.zone]?.machineTypes ?? []
1646
).includes(prefix);
1647
const regionToCost =
1648
priceData.machineTypes[machineType][
1649
configuration.spot ? "spot" : "prices"
1650
] ?? {};
1651
const regionHasMachineType = regionToCost[configuration.region] != null;
1652
1653
if (!regionHasMachineType) {
1654
// Our machine type is not in the currently selected region,
1655
// so find cheapest region with our requested machine type.
1656
let price = 1e8;
1657
for (const region in regionToCost) {
1658
if (regionToCost[region] < price) {
1659
price = regionToCost[region];
1660
configuration["region"] = changes["region"] = region;
1661
// since we changed the region:
1662
zoneHasMachineType = false;
1663
}
1664
}
1665
}
1666
if (!zoneHasMachineType) {
1667
// now the region has the machine type, but the zone doesn't (or
1668
// region changed so zone has to change).
1669
// So we find some zone with the machine in that region
1670
for (const zone in priceData.zones) {
1671
if (zone.startsWith(configuration["region"])) {
1672
if ((priceData.zones[zone]?.machineTypes ?? []).includes(prefix)) {
1673
configuration["zone"] = changes["zone"] = zone;
1674
break;
1675
}
1676
}
1677
}
1678
}
1679
1680
if (configuration.acceleratorType && configuration.acceleratorCount) {
1681
if (priceData.accelerators[configuration.acceleratorType] == null) {
1682
// The accelerator type no longer exists in the pricing data (e.g., maybe it was deprecated),
1683
// so replace it by one that exists.
1684
for (const type in priceData.accelerators) {
1685
configuration.acceleratorType = changes.acceleratorType = type;
1686
break;
1687
}
1688
}
1689
// have a GPU -- make sure zone works
1690
if (
1691
!priceData.accelerators[configuration.acceleratorType].prices[
1692
configuration.zone
1693
]
1694
) {
1695
// try to find a different zone in the region that works
1696
let fixed = false;
1697
const region = zoneToRegion(configuration["zone"]);
1698
for (const zone in priceData.accelerators[configuration.acceleratorType]
1699
?.prices) {
1700
if (zone.startsWith(region)) {
1701
fixed = true;
1702
configuration.zone = changes.zone = zone;
1703
break;
1704
}
1705
}
1706
if (!fixed) {
1707
// just choose cheapest zone in some region
1708
const zone = cheapestZone(
1709
priceData.accelerators[configuration.acceleratorType][
1710
configuration.spot ? "spot" : "prices"
1711
],
1712
);
1713
configuration.zone = changes.zone = zone;
1714
configuration.region = changes.region = zoneToRegion(zone);
1715
}
1716
}
1717
}
1718
}
1719
1720
function zoneToRegion(zone: string): string {
1721
const i = zone.lastIndexOf("-");
1722
return zone.slice(0, i);
1723
}
1724
1725
function Network({ setConfig, configuration, loading, priceData }) {
1726
const [externalIp, setExternalIp] = useState<boolean>(
1727
configuration.externalIp ?? true,
1728
);
1729
useEffect(() => {
1730
setExternalIp(configuration.externalIp ?? true);
1731
}, [configuration.externalIp]);
1732
1733
return (
1734
<div>
1735
<div style={{ color: "#666", marginBottom: "5px" }}>
1736
<b>
1737
<Icon name="network" /> Network
1738
</b>
1739
<br />
1740
All compute servers on Google cloud have full network access with
1741
unlimited data transfer in for free. Data transfer out{" "}
1742
<b>costs {currency(DATA_TRANSFER_OUT_COST_PER_GiB)}/GiB</b>.
1743
</div>
1744
<Checkbox
1745
checked={externalIp}
1746
disabled={
1747
true /* compute servers can't work without external ip or Cloud NAT (which costs a lot), so changing this always disabled. Before: disabled || (state ?? "deprovisioned") != "deprovisioned"*/
1748
}
1749
onChange={() => {
1750
setExternalIp(!externalIp);
1751
setConfig({ externalIp: !externalIp });
1752
}}
1753
>
1754
External IP Address
1755
</Checkbox>
1756
<div style={{ marginTop: "5px" }}>
1757
<Typography.Paragraph
1758
style={{ color: "#666" }}
1759
ellipsis={{
1760
expandable: true,
1761
rows: 2,
1762
symbol: "more",
1763
}}
1764
>
1765
{/* TODO: we can and will in theory support all this without external
1766
ip using a gateway. E.g., google cloud shell has ssh to host, etc. */}
1767
An external IP address is required and costs{" "}
1768
{configuration.spot
1769
? `${currency(
1770
markup({ cost: EXTERNAL_IP_COST.spot, priceData }),
1771
)}/hour`
1772
: `${currency(
1773
markup({
1774
cost: EXTERNAL_IP_COST.standard,
1775
priceData,
1776
}),
1777
)}/hour`}{" "}
1778
while the VM is running (there is no charge when not running).
1779
</Typography.Paragraph>
1780
</div>
1781
{externalIp && (
1782
<DNS
1783
setConfig={setConfig}
1784
configuration={configuration}
1785
loading={loading}
1786
/>
1787
)}
1788
</div>
1789
);
1790
}
1791
1792
function cheapestZone(costs: { [zone: string]: number }): string {
1793
let price = 99999999999999999;
1794
let choice = "";
1795
for (const zone in costs) {
1796
if (costs[zone] < price) {
1797
choice = zone;
1798
price = costs[zone];
1799
}
1800
}
1801
return choice;
1802
}
1803
1804
function CostPerHour({
1805
cost,
1806
extra,
1807
style,
1808
}: {
1809
cost?: number;
1810
extra?;
1811
style?;
1812
}) {
1813
if (cost == null) {
1814
return null;
1815
}
1816
return (
1817
<div style={{ fontFamily: "monospace", ...style }}>
1818
{currency(cost)}/hour
1819
{extra}
1820
</div>
1821
);
1822
}
1823
1824
function Admin({ id, configuration, loading, template }) {
1825
const isAdmin = useTypedRedux("account", "is_admin");
1826
const [error, setError] = useState<string>("");
1827
const [calling, setCalling] = useState<boolean>(false);
1828
if (!isAdmin) {
1829
return null;
1830
}
1831
return (
1832
<div>
1833
<div style={{ color: "#666", marginBottom: "5px" }}>
1834
<b>
1835
<Icon name="users" /> Admin
1836
</b>
1837
<br />
1838
Settings and functionality only available to admins.
1839
<br />
1840
<ShowError error={error} setError={setError} />
1841
<Tooltip title="Once you have tested the currently selected image, click this button to mark it as tested.">
1842
<Button
1843
disabled={loading || !id || calling}
1844
onClick={async () => {
1845
try {
1846
setCalling(true);
1847
await setImageTested({ id, tested: true });
1848
// force reload to database via GCP api call
1849
await reloadImages("compute_servers_images_google", true);
1850
} catch (err) {
1851
setError(`${err}`);
1852
} finally {
1853
setCalling(false);
1854
}
1855
}}
1856
>
1857
Mark Google Cloud Image Tested{" "}
1858
{calling && <Spin style={{ marginLeft: "15px" }} />}
1859
</Button>
1860
</Tooltip>
1861
<pre>
1862
id={id}, configuration={JSON.stringify(configuration, undefined, 2)}
1863
</pre>
1864
<Template id={id} template={template} />
1865
</div>
1866
</div>
1867
);
1868
}
1869
1870