Path: blob/master/src/packages/frontend/compute/action.tsx
1503 views
import { Alert, Button, Modal, Popconfirm, Popover, Spin } from "antd";1import { useEffect, useState } from "react";2import { redux, useStore } from "@cocalc/frontend/app-framework";3import { A, CopyToClipBoard, Icon } from "@cocalc/frontend/components";4import ShowError from "@cocalc/frontend/components/error";5import { appBasePath } from "@cocalc/frontend/customize/app-base-path";6import { CancelText } from "@cocalc/frontend/i18n/components";7import MoneyStatistic from "@cocalc/frontend/purchases/money-statistic";8import confirmStartComputeServer from "@cocalc/frontend/purchases/pay-as-you-go/confirm-start-compute-server";9import { webapp_client } from "@cocalc/frontend/webapp-client";10import {11ACTION_INFO,12STATE_INFO,13getTargetState,14getArchitecture,15} from "@cocalc/util/db-schema/compute-servers";16import { computeServerAction, getApiKey } from "./api";17import costPerHour from "./cost";1819export default function getActions({20id,21state,22editable,23setError,24configuration,25editModal,26type,27project_id,28}): React.JSX.Element[] {29if (!editable && !configuration?.allowCollaboratorControl) {30return [];31}32const s = STATE_INFO[state ?? "off"];33if (s == null) {34return [];35}36if ((s.actions ?? []).length == 0) {37return [];38}39const v: React.JSX.Element[] = [];40for (const action of s.actions) {41if (42!editable &&43!["stop", "start", "suspend", "resume", "reboot", "deprovision"].includes(44action,45)46) {47// non-owner can only do start/stop/suspend/resume/deprovision -- NOT delete.48continue;49}50const a = ACTION_INFO[action];51if (!a) continue;52if (action == "suspend") {53if (configuration.cloud != "google-cloud") {54continue;55}56if (getArchitecture(configuration) == "arm64") {57// TODO: suspend/resume breaks the clock badly on ARM64, and I haven't58// figured out a workaround, so don't support it for now. I guess this59// is a GCP bug.60continue;61}62// must have no gpu and <= 208GB of RAM -- https://cloud.google.com/compute/docs/instances/suspend-resume-instance63if (configuration.acceleratorType) {64continue;65}66// [ ] TODO: we don't have an easy way to check the RAM requirement right now.67}68if (!editModal && configuration.ephemeral && action == "stop") {69continue;70}71const {72label,73icon,74tip,75description,76confirm,77danger,78confirmMessage,79clouds,80} = a;81if (danger && !configuration.ephemeral && !editModal) {82continue;83}84if (clouds && !clouds.includes(configuration.cloud)) {85continue;86}87v.push(88<ActionButton89style={v.length > 0 ? { marginLeft: "5px" } : undefined}90key={action}91id={id}92action={action}93label={label}94icon={icon}95tip={tip}96editable={editable}97description={description}98setError={setError}99confirm={confirm}100configuration={configuration}101danger={danger}102confirmMessage={confirmMessage}103type={type}104state={state ?? "off"}105project_id={project_id}106/>,107);108}109return v;110}111112function ActionButton({113id,114action,115icon,116label,117editable,118description,119tip,120setError,121confirm,122confirmMessage,123configuration,124danger,125type,126style,127state,128project_id,129}) {130const [showOnPremStart, setShowOnPremStart] = useState<boolean>(false);131const [showOnPremStop, setShowOnPremStop] = useState<boolean>(false);132const [showOnPremDeprovision, setShowOnPremDeprovision] =133useState<boolean>(false);134const [cost_per_hour, setCostPerHour] = useState<number | null>(null);135const [popConfirm, setPopConfirm] = useState<boolean>(false);136const updateCost = async () => {137try {138const c = await costPerHour({139configuration,140state: getTargetState(action),141});142setCostPerHour(c);143return c;144} catch (err) {145setError(`Unable to compute cost: ${err}`);146setCostPerHour(null);147return null;148}149};150useEffect(() => {151if (configuration == null) return;152updateCost();153}, [configuration, action]);154const customize = useStore("customize");155const [understand, setUnderstand] = useState<boolean>(false);156const [doing, setDoing] = useState<boolean>(!STATE_INFO[state]?.stable);157158const doAction = async () => {159if (action == "start") {160// check version161const required =162customize?.get("version_compute_server_min_project") ?? 0;163if (required > 0) {164if (redux.getStore("projects").get_state(project_id) == "running") {165// only check if running -- if not running, the project will obviously166// not need a restart, since it isn't even running167const api = await webapp_client.project_client.api(project_id);168const version = await api.version(1690 /* want version of the home base! */,170);171if (version < required) {172setError(173"You must restart your project to upgrade it to the latest version.",174);175return;176}177}178}179}180181if (configuration.cloud == "onprem") {182if (action == "start") {183setShowOnPremStart(true);184} else if (action == "stop") {185setShowOnPremStop(true);186} else if (action == "deprovision") {187setShowOnPremDeprovision(true);188}189190// right now user has to copy paste191return;192}193194try {195setError("");196setDoing(true);197if (editable && (action == "start" || action == "resume")) {198let c = cost_per_hour;199if (c == null) {200c = await updateCost();201if (c == null) {202// error would be displayed above.203return;204}205}206await confirmStartComputeServer({ id, cost_per_hour: c });207}208await computeServerAction({ id, action });209} catch (err) {210setError(`${err}`);211} finally {212setDoing(false);213}214};215useEffect(() => {216setDoing(!STATE_INFO[state]?.stable);217}, [action, state]);218219if (configuration == null) {220return null;221}222223let button = (224<Button225style={style}226disabled={doing}227type={type}228onClick={!confirm ? doAction : undefined}229danger={danger}230>231<Icon name={icon} /> {label}{" "}232{doing && (233<>234<div style={{ display: "inline-block", width: "10px" }} />235<Spin />236</>237)}238</Button>239);240if (confirm) {241button = (242<Popconfirm243onOpenChange={setPopConfirm}244placement="right"245okButtonProps={{246disabled: !configuration.ephemeral && danger && !understand,247}}248title={249<div>250{label} - Are you sure?251{action == "deprovision" && (252<Alert253showIcon254style={{ margin: "15px 0", maxWidth: "400px" }}255type="warning"256message={257"This will delete the boot disk! This does not touch the files in your HOME BASE (what you see when not using a compute server). This permanently deletes EVERYTHING stored on the compute server, especially in any fast local directories."258}259/>260)}261{action == "stop" && (262<Alert263showIcon264style={{ margin: "15px 0" }}265type="info"266message={`This will safely turn off the VM${267editable ? ", and allow you to edit its configuration." : "."268}`}269/>270)}271{!configuration.ephemeral && danger && (272<div>273{/* ATTN: Not using a checkbox here to WORKAROUND A BUG IN CHROME that I see after a day or so! */}274<Button onClick={() => setUnderstand(!understand)} type="text">275<Icon276name={understand ? "check-square" : "square"}277style={{ marginRight: "5px" }}278/>279{confirmMessage ??280"I understand that this may result in data loss."}281</Button>282</div>283)}284</div>285}286onConfirm={doAction}287okText={`Yes, ${label} VM`}288cancelText={<CancelText />}289>290{button}291</Popconfirm>292);293}294295const content = (296<>297{button}298{showOnPremStart && action == "start" && (299<OnPremGuide300action={action}301setShow={setShowOnPremStart}302configuration={configuration}303id={id}304title={305<>306<Icon name="server" /> Connect Your Virtual Machine to CoCalc307</>308}309/>310)}311{showOnPremStop && action == "stop" && (312<OnPremGuide313action={action}314setShow={setShowOnPremStop}315configuration={configuration}316id={id}317title={318<>319<Icon name="stop" /> Disconnect Your Virtual Machine from CoCalc320</>321}322/>323)}324{showOnPremDeprovision && action == "deprovision" && (325<OnPremGuide326action={action}327setShow={setShowOnPremDeprovision}328configuration={configuration}329id={id}330title={331<div style={{ color: "darkred" }}>332<Icon name="trash" /> Disconnect Your Virtual Machine and Remove333Files334</div>335}336/>337)}338</>339);340341// Do NOT use popover in case we're doing a popconfirm.342// Two popovers at once is just unprofessional and hard to use.343// That's why the "open={popConfirm ? false : undefined}" below344345return (346<Popover347open={popConfirm ? false : undefined}348placement="left"349key={action}350mouseEnterDelay={1}351title={352<div>353<Icon name={icon} /> {tip}354</div>355}356content={357<div style={{ width: "400px" }}>358{description} {editable && <>You will be charged:</>}359{!editable && <>The owner of this compute server will be charged:</>}360{cost_per_hour != null && (361<div style={{ textAlign: "center" }}>362<MoneyStatistic363value={cost_per_hour}364title="Cost per hour"365costPerMonth={730 * cost_per_hour}366/>367</div>368)}369</div>370}371>372{content}373</Popover>374);375}376377function OnPremGuide({ setShow, configuration, id, title, action }) {378const [apiKey, setApiKey] = useState<string | null>(null);379const [error, setError] = useState<string>("");380useEffect(() => {381(async () => {382try {383setError("");384setApiKey(await getApiKey({ id }));385} catch (err) {386setError(`${err}`);387}388})();389}, []);390return (391<Modal392width={800}393title={title}394open={true}395onCancel={() => {396setShow(false);397}}398onOk={() => {399setShow(false);400}}401>402{action == "start" && (403<div>404You can connect any{" "}405<b>Ubuntu 22.04 or 24.04 Linux Virtual Machine (VM)</b> with root406access to this project. This VM can be anywhere (your laptop or a407cloud hosting providing). Your VM needs to be able to create outgoing408network connections, but does NOT need to have a public ip address.409<Alert410style={{ margin: "15px 0" }}411type="warning"412showIcon413message={<b>USE AN UBUNTU 22.04 or 24.04 VIRTUAL MACHINE</b>}414description={415<div>416You can use any{" "}417<u>418<b>419<A href="https://multipass.run/">UBUNTU VIRTUAL MACHINE</A>420</b>421</u>{" "}422that you have a root acount on.{" "}423<A href="https://multipass.run/">424Multipass is a very easy and free way to install one or more425minimal Ubuntu VM's on Windows, Mac, and Linux.426</A>{" "}427After you install Multipass, create a VM by pasting this in a428terminal on your computer (you can increase the cpu, memory and429disk):430<CopyToClipBoard431inputWidth="600px"432style={{ marginTop: "10px" }}433value={`multipass launch --name compute-server-${id} --cpus 1 --memory 4G --disk 25G`}434/>435<br />436Then launch a terminal shell running in the VM:437<CopyToClipBoard438inputWidth="600px"439style={{ marginTop: "10px" }}440value={`multipass shell compute-server-${id}`}441/>442</div>443}444/>445{configuration.gpu && (446<span>447Since you clicked GPU, you must also have an NVIDIA GPU and the448Cuda drivers installed and working.{" "}449</span>450)}451</div>452)}453<div style={{ marginTop: "15px" }}>454{apiKey && (455<div>456<div style={{ marginBottom: "10px" }}>457Copy and paste the following into a terminal shell on your{" "}458<b>Ubuntu Virtual Machine</b>:459</div>460<CopyToClipBoard461inputWidth={"700px"}462value={`curl -fsS https://${window.location.host}${463appBasePath.length > 1 ? appBasePath : ""464}/compute/${id}/onprem/${action}/${apiKey} | sudo bash`}465/>466</div>467)}468{!apiKey && !error && <Spin />}469<ShowError error={error} setError={setError} />470</div>471{action == "stop" && (472<div>473This will disconnect your VM from CoCalc and stop it from syncing474files, running terminals and Jupyter notebooks. Files and software you475installed will not be deleted and you can start the compute server476later.477<Alert478style={{ margin: "15px 0" }}479type="warning"480showIcon481message={482<b>483If you're using{" "}484<A href="https://multipass.run/">Multipass...</A>485</b>486}487description={488<div>489<CopyToClipBoard490value={`multipass stop compute-server-${id}`}491/>492<br />493HINT: If you ever need to enlarge the disk, do this:494<CopyToClipBoard495inputWidth="600px"496value={`multipass stop compute-server-${id} && multipass set local.compute-server-${id}.disk=30G`}497/>498</div>499}500/>501</div>502)}503{action == "deprovision" && (504<div>505This will disconnect your VM from CoCalc, and permanently delete any506local files and software you installed into your compute server.507<Alert508style={{ margin: "15px 0" }}509type="warning"510showIcon511message={512<b>513If you're using{" "}514<A href="https://multipass.run/">Multipass...</A>515</b>516}517description={518<CopyToClipBoard519value={`multipass delete compute-server-${id}`}520/>521}522/>523</div>524)}525{action == "deprovision" && (526<div style={{ marginTop: "15px" }}>527NOTE: This does not delete Docker or any Docker images. Run this to528delete all unused Docker images:529<br />530<CopyToClipBoard value="docker image prune -a" />531</div>532)}533</Modal>534);535}536537538