diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 282a8016c1d..a52cfee08aa 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -1,9 +1,11 @@ -import { guides } from "./plugins/sidebar"; +import { generateSidebar } from "./plugins/sidebar"; + +export const DOCS_SRC_DIR = new URL("./../", import.meta.url).pathname; export default { title: "Cloud Manager Docs", description: "Akamai Cloud Manger Documentation", - srcDir: "./", + srcDir: DOCS_SRC_DIR, base: "/manager/", themeConfig: { logo: "/akamai-wave.svg", @@ -15,12 +17,7 @@ export default { search: { provider: "local", }, - sidebar: [ - { - text: "Development Guide", - items: guides, - }, - ], + sidebar: generateSidebar(DOCS_SRC_DIR), socialLinks: [ { icon: "github", link: "https://github.com/linode/manager" }, ], diff --git a/docs/.vitepress/plugins/sidebar.ts b/docs/.vitepress/plugins/sidebar.ts index 94152e87047..228af1d39da 100644 --- a/docs/.vitepress/plugins/sidebar.ts +++ b/docs/.vitepress/plugins/sidebar.ts @@ -1,38 +1,89 @@ -import * as fs from "fs"; -import * as path from "path"; +import { readdirSync } from "fs"; +import { join } from "path"; +import { DOCS_SRC_DIR } from "../config"; -const DEVELOPMENT_GUIDE_PATH = "./docs/development-guide"; +type LinkItem = { text: string; link: string }; -interface MarkdownInfo { - text: string; - link: string; +type SidebarItem = + | { + text: string; + collapsed?: boolean; + items: SidebarItem[] | LinkItem[]; + } + | LinkItem; + +const exclude = [ + "cache", + "public", + "PULL_REQUEST_TEMPLATE.md", + ".vitepress", + "index.md", +]; + +const replacements = [ + ["-", " "], + ["_", " "], + [".md", ""], +]; + +function isPathIgnored(path: string) { + for (const item of exclude) { + if (path.includes(item)) { + return true; + } + } + return false; +} + +function capitalize(s: string) { + return ( + s.substring(0, 1).toUpperCase() + s.substring(1, s.length).toLowerCase() + ); } /** - * Aggregates the pages in the development-guide and populates the left sidebar. + * Given a file name, this function returns a formatted title. */ -const scanDirectory = (directoryPath: string): MarkdownInfo[] => { - const markdownFiles = fs - .readdirSync(directoryPath) - .filter((file) => file.endsWith(".md")); - const markdownInfoArray: MarkdownInfo[] = []; +function formatSidebarItemText(fileName: string) { + // removes -, _, and .md from files names to generate the title + for (const [from, to] of replacements) { + fileName = fileName.replaceAll(from, to); + } + // Removes any number prefix. This allows us to order things by putting numbers in file names. + fileName = fileName.replace(/^[0-9]*/, ""); + // Capitalizes each word in the file name + fileName = fileName.split(" ").map(capitalize).join(" "); + return fileName; +} - markdownFiles.forEach((file) => { - const filePath = path.join(directoryPath, file); - const fileContent = fs.readFileSync(filePath, "utf-8"); +/** + * Generates a VitePress sidebar by recursively traversing the given directory. + */ +export function generateSidebar(dir: string) { + const files = readdirSync(dir, { withFileTypes: true }); - const titleMatch = fileContent.match(/^#\s+(.*)/m); - const title = titleMatch ? titleMatch[1] : "Untitled"; + const sidebar: SidebarItem[] = []; - const markdownInfo: MarkdownInfo = { - text: title, - link: `/development-guide/${file}`, - }; + for (const file of files) { + const filepath = join(dir, file.name); - markdownInfoArray.push(markdownInfo); - }); + if (isPathIgnored(filepath)) { + continue; + } - return markdownInfoArray; -}; + if (file.isDirectory()) { + sidebar.push({ + text: formatSidebarItemText(file.name), + collapsed: false, + items: generateSidebar(filepath), + }); + } else { + sidebar.push({ + text: formatSidebarItemText(file.name), + link: filepath.split(DOCS_SRC_DIR)[1], + }); + } + } -export const guides = scanDirectory(DEVELOPMENT_GUIDE_PATH); + return sidebar; +} diff --git a/docs/.vitepress/tsconfig.json b/docs/.vitepress/tsconfig.json new file mode 100644 index 00000000000..163fc24b10e --- /dev/null +++ b/docs/.vitepress/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "module": "ESNext", + } +} diff --git a/docs/development-guide/02-component-structure.md b/docs/development-guide/02-component-structure.md index 55a0f57b14e..004e4ba0923 100644 --- a/docs/development-guide/02-component-structure.md +++ b/docs/development-guide/02-component-structure.md @@ -61,7 +61,9 @@ export const capitalize = (s: string) => { #### Composition -When building a large component, it is recommended to break it down and avoid writing several components within the same file. It improves readability and testability. Components should, in most cases, come with their own unit test, although they can be skipped if an e2e suite is covering the functionality. +When building a large component, it is recommended to break it down and avoid writing several components within the same file. It improves readability and testability. It is also best to avoid same-file render functions (e.g. `renderTableBody`), in favor of extracting the JSX into its own component. In addition to improved readability and testability, this practice makes components less brittle and more extensible. + +Components should, in most cases, come with their own unit test, although they can be skipped if an e2e suite is covering the functionality. Utilities should almost always feature a unit test. #### Styles @@ -85,6 +87,8 @@ export const interface MyComponentProps { const MyComponent = (props: MyComponentProps) { ... } ``` - When it comes to components located in the `src/features/` directory, you can use the name `Props` for their types or interfaces, unless exporting is necessary. In such cases, name the type or interface after the component name. +- Define props as required, rather than optional, as often as possible for data relying on API responses (which can be `undefined`). In the case of `undefined` props, error handling - such as early return statements - can be done in the HOC. This allows all child components to expect data, avoiding extra conditionals or convoluted logic. + #### Function Component Definition - Prefer function components over class components. diff --git a/docs/development-guide/04-component-library.md b/docs/development-guide/04-component-library.md index b66e19046eb..2be52331b2d 100644 --- a/docs/development-guide/04-component-library.md +++ b/docs/development-guide/04-component-library.md @@ -2,7 +2,7 @@ ## Material-UI -We use [Material-UI](https://mui.com/material-ui/getting-started/overview/) as the primary component library for Cloud Manager. The library contains many UI primitives like `` and `` as well as a layout system with the `` component. +We use [Material-UI](https://mui.com/material-ui/getting-started/overview/) as the primary component library for Cloud Manager. The library contains many UI primitives like `` and ``, as well as a layout system with the `` component. All MUI components have abstractions in the Cloud Manager codebase, meaning you will use relative imports to use them instead of importing from MUI directly: @@ -37,7 +37,7 @@ A color, font, svg icon, or other simple styling convention. ##### Element -A basic HTML element wrapped in a react component, or a small component that is not normally used on its own. +A basic HTML element wrapped in a React component, or a small component that is not normally used on its own. ##### Component @@ -45,10 +45,8 @@ A composition of Core Styles and Elements. Normally with some code that defines ##### Feature -A Composition of Core Styles, Elements, and Components that defines a verticle slice of functionality. An example of a Feature is the Payment Method Row it combines Components, Elements, and Core Styles like Buttons, Action Menus, Icons, Typography, etc. +A Composition of Core Styles, Elements, and Components that defines a vertical slice of functionality. An example of a Feature is the Payment Method Row; it combines Components, Elements, and Core Styles like Buttons, Action Menus, Icons, Typography, etc. #### Best Practices -Our stories are in the process of being updated to the latest Storybook 7.0 format. -We currently use MDX both for documentation and for defining stories in the same `.stories.mdx` file. However, Storybook has deprecated this functionality and they plan to remove it in a future version of Storybook. -As we begin to move away from the MDX format, please refer to Storybook's [documentation](https://storybook.js.org/docs/react/writing-docs/introduction) for how to write stories in the CSF format. +Please refer to Storybook's [documentation](https://storybook.js.org/docs/react/writing-docs/introduction) for how to write stories in the CSF format. diff --git a/docs/development-guide/10-local-dev-tools.md b/docs/development-guide/10-local-dev-tools.md index 8dca8ebe66d..172269fb849 100644 --- a/docs/development-guide/10-local-dev-tools.md +++ b/docs/development-guide/10-local-dev-tools.md @@ -1,10 +1,24 @@ # Local Dev Tools -To facilitate development and debugging, Cloud Manager includes a "Dev Tools" mode. Currently this mode is used for feature flag toggling, data mocking, and environment switching. +To facilitate development and debugging, Cloud Manager includes a "Dev Tools" mode. Currently this mode is used for feature flag toggling, data mocking, and theme & environment switching. + +In order to access the dev tools, hover or click (mobile) on the 🛠icon in the lower left corner of your browser window. The icon will be colored green if MSW is enabled. This mode is enabled by default while running the development server. To disable it, add `?dev-tools=false` to the URL, or write `dev-tools: false` to local storage. -This mode is disabled by default in production builds, but can be enabled by adding `?dev-tools=true` to the URL, or `dev-tools: true` to local storage. +This mode is disabled by default in production builds. + +## Feature Flags + +The display of the Flags in dev tools is defined in the `options` array in `FeatureFlagTool.tsx`. While it is convenient to add those switches to the dev tools, it is not always necessary as they can clutter the UI. Additionally, it is important to clean them up once the feature has been battle tested in production. + +The flags on/off values are stored in local storage for convenience and will be remembered on reload or app restart. + +By default, the boolean flags checkboxes represent their true values as returned by Launch Darkly (dev environment). Hitting the reset button will bring them back to those default values and clear local storage. + +## Theme Select + +The theme select in dev tools is a convenient way to store the theme choice while MSW is enabled (it won't affect your actual Application settings/preferences). The default is "system". ## Writing a new tool diff --git a/package.json b/package.json index 9ed95835e54..7a34162fe1a 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "coverage": "yarn workspace linode-manager coverage", "coverage:summary": "yarn workspace linode-manager coverage:summary", "junit:summary": "ts-node scripts/junit-summary/index.ts", - "docs": "bunx vitepress@1.0.0-rc.35 dev docs" + "docs": "bunx vitepress@1.0.0-rc.44 dev docs" }, "resolutions": { "@babel/traverse": "^7.23.3", @@ -58,11 +58,11 @@ "lodash": "^4.17.21", "glob-parent": "^5.1.2", "hosted-git-info": "^5.0.0", - "@types/react": "^17", "yaml": "^2.3.0", "word-wrap": "^1.2.4", "semver": "^7.5.2", - "tough-cookie": "^4.1.3" + "tough-cookie": "^4.1.3", + "jackspeak": "2.1.1" }, "workspaces": { "packages": [ diff --git a/packages/api-v4/CHANGELOG.md b/packages/api-v4/CHANGELOG.md index 7e09e127e93..7e88e1546d2 100644 --- a/packages/api-v4/CHANGELOG.md +++ b/packages/api-v4/CHANGELOG.md @@ -1,3 +1,13 @@ +## [2024-03-04] - v0.111.0 + +### Changed: + +- Rename `database_scale` type to `database_resize` ([#10193](https://github.com/linode/manager/pull/10193)) + +### Upcoming Features: + +- Accept placement group in Linode create payload ([#10195](https://github.com/linode/manager/pull/10195)) + ## [2024-02-20] - v0.110.0 ### Upcoming Features: diff --git a/packages/api-v4/package.json b/packages/api-v4/package.json index 4d5475a0cfe..4b13695ece2 100644 --- a/packages/api-v4/package.json +++ b/packages/api-v4/package.json @@ -1,6 +1,6 @@ { "name": "@linode/api-v4", - "version": "0.110.0", + "version": "0.111.0", "homepage": "https://github.com/linode/manager/tree/develop/packages/api-v4", "bugs": { "url": "https://github.com/linode/manager/issues" diff --git a/packages/api-v4/src/account/types.ts b/packages/api-v4/src/account/types.ts index 3800001d242..81bacfa7297 100644 --- a/packages/api-v4/src/account/types.ts +++ b/packages/api-v4/src/account/types.ts @@ -287,7 +287,7 @@ export type EventAction = | 'community_question_reply' | 'credit_card_updated' | 'database_low_disk_space' - | 'database_scale' + | 'database_resize' | 'database_backup_restore' | 'database_create' | 'database_credentials_reset' diff --git a/packages/api-v4/src/linodes/types.ts b/packages/api-v4/src/linodes/types.ts index 73634125016..cfbd0dd089b 100644 --- a/packages/api-v4/src/linodes/types.ts +++ b/packages/api-v4/src/linodes/types.ts @@ -1,7 +1,7 @@ import type { Region } from '../regions'; import type { IPAddress, IPRange } from '../networking/types'; import type { SSHKey } from '../profile/types'; -import type { PlacementGroup } from '../placement-groups/types'; +import type { PlacementGroupPayload } from '../placement-groups/types'; export type Hypervisor = 'kvm' | 'zen'; @@ -24,9 +24,7 @@ export interface Linode { ipv4: string[]; ipv6: string | null; label: string; - placement_groups: - | [Pick] // While the API returns an array of PlacementGroup objects for future proofing, we only support one PlacementGroup per Linode at this time, hence the tuple. - | []; + placement_group?: PlacementGroupPayload; // If not in a placement group, this will be excluded from the response. type: string | null; status: LinodeStatus; updated: string; @@ -230,10 +228,13 @@ export interface Kernel { label: string; version: string; kvm: boolean; - xen: boolean; architecture: KernelArchitecture; pvops: boolean; deprecated: boolean; + /** + * @example 2009-10-26T04:00:00 + */ + built: string; } export interface NetStats { @@ -338,6 +339,16 @@ export interface UserData { user_data: string | null; } +export interface CreateLinodePlacementGroupPayload { + id: number; + /** + * This parameter is silent in Cloud Manager, but still needs to be represented in the API types. + * + * @default false + */ + compliant_only?: boolean; +} + export interface CreateLinodeRequest { type?: string; region?: string; @@ -357,6 +368,7 @@ export interface CreateLinodeRequest { interfaces?: InterfacePayload[]; metadata?: UserData; firewall_id?: number; + placement_group?: CreateLinodePlacementGroupPayload; } export type RescueRequestObject = Pick< diff --git a/packages/api-v4/src/placement-groups/placement-groups.ts b/packages/api-v4/src/placement-groups/placement-groups.ts index 16cd63182fb..123149c8589 100644 --- a/packages/api-v4/src/placement-groups/placement-groups.ts +++ b/packages/api-v4/src/placement-groups/placement-groups.ts @@ -17,7 +17,7 @@ import type { CreatePlacementGroupPayload, PlacementGroup, UnassignLinodesFromPlacementGroupPayload, - RenamePlacementGroupPayload, + UpdatePlacementGroupPayload, } from './types'; /** @@ -72,7 +72,7 @@ export const createPlacementGroup = (data: CreatePlacementGroupPayload) => */ export const renamePlacementGroup = ( placementGroupId: number, - data: RenamePlacementGroupPayload + data: UpdatePlacementGroupPayload ) => Request( setURL( diff --git a/packages/api-v4/src/placement-groups/types.ts b/packages/api-v4/src/placement-groups/types.ts index 63f64584004..df593e53bdc 100644 --- a/packages/api-v4/src/placement-groups/types.ts +++ b/packages/api-v4/src/placement-groups/types.ts @@ -6,6 +6,7 @@ export const AFFINITY_TYPES = { } as const; export type AffinityType = keyof typeof AFFINITY_TYPES; +export type AffinityEnforcement = 'Strict' | 'Flexible'; export interface PlacementGroup { id: number; @@ -13,25 +14,37 @@ export interface PlacementGroup { region: Region['id']; affinity_type: AffinityType; is_compliant: boolean; - linode_ids: number[]; + linodes: { + linode: number; + is_compliant: boolean; + }[]; + is_strict: boolean; } -// The `strict` parameter specifies whether placement groups should be ignored when looking for a host. -// TODO VM_Placement: figure out the values for each create flow (create, clone, migrate etc) -export type CreatePlacementGroupPayload = Pick< +export type PlacementGroupPayload = Pick< PlacementGroup, - 'label' | 'affinity_type' | 'region' -> & { strict: boolean }; + 'id' | 'label' | 'affinity_type' | 'is_strict' +>; -export type RenamePlacementGroupPayload = Pick; +export type CreatePlacementGroupPayload = Omit & { + region: Region['id']; +}; + +export type UpdatePlacementGroupPayload = Pick; /** * Since the API expects an array of ONE linode id, we'll use a tuple here. */ export type AssignLinodesToPlacementGroupPayload = { linodes: [number]; - strict: boolean; + /** + * This parameter is silent in Cloud Manager, but still needs to be represented in the API types. + * + * @default false + */ + compliant_only?: boolean; }; + export type UnassignLinodesFromPlacementGroupPayload = { linodes: [number]; }; diff --git a/packages/api-v4/src/regions/types.ts b/packages/api-v4/src/regions/types.ts index 23204247879..84871a6a6b0 100644 --- a/packages/api-v4/src/regions/types.ts +++ b/packages/api-v4/src/regions/types.ts @@ -24,6 +24,8 @@ export interface DNSResolvers { export type RegionStatus = 'ok' | 'outage'; +export type RegionSite = 'core' | 'edge'; + export interface Region { id: string; label: string; @@ -33,6 +35,7 @@ export interface Region { maximum_vms_per_pg: number; status: RegionStatus; resolvers: DNSResolvers; + site_type: RegionSite; } export interface RegionAvailability { diff --git a/packages/manager/CHANGELOG.md b/packages/manager/CHANGELOG.md index 88934156e35..3517ca79c82 100644 --- a/packages/manager/CHANGELOG.md +++ b/packages/manager/CHANGELOG.md @@ -4,6 +4,61 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [2024-03-04] - v1.114.0 + +### Added: + +- Reintroduce NVMe Volume Upgrades ([#10229](https://github.com/linode/manager/pull/10229)) + +### Changed: + +- Improve dev tools UI ([#10220](https://github.com/linode/manager/pull/10220)) +- ACLB beta region from `Washington, DC` to `Miami, FL` ([#10232](https://github.com/linode/manager/pull/10232)) + +### Fixed: + +- Incorrect units in Linode Network Graph Tooltip ([#10197](https://github.com/linode/manager/pull/10197)) +- Disabled `Add` button once a node pool is added to kubernetes cluster in Create flow ([#10215](https://github.com/linode/manager/pull/10215)) +- Invalid VPC scope with a Select All > Read Only selection in Create PAT drawer ([#10226](https://github.com/linode/manager/pull/10226)) +- Disabled styles for Textfield input ([#10231](https://github.com/linode/manager/pull/10231)) +- LinodeVolumeCreateForm crash ([#10235](https://github.com/linode/manager/pull/10235)) + +### Tech Stories: + +- Update to React 18 ([#10169](https://github.com/linode/manager/pull/10169)) +- Improve LinodeActionMenu restricted user experience ([#10199](https://github.com/linode/manager/pull/10199)) +- Convert isRestrictedGlobalGrantType to Hook ([#10203](https://github.com/linode/manager/pull/10203)) +- Update Storybook to latest to resolve CVE-2023-42282 ([#10212](https://github.com/linode/manager/pull/10212)) +- Generate docs site sidebar based on folder structure ([#10214](https://github.com/linode/manager/pull/10214)) +- Clean up `new QueryClient()` pattern in unit tests ([#10217](https://github.com/linode/manager/pull/10217)) +- Remove build time API caching ([#10219](https://github.com/linode/manager/pull/10219)) +- Clean up `Chip` component ([#10223](https://github.com/linode/manager/pull/10223)) + +### Tests: + +- Add Cypress tests for account switching from Parent to Child ([#10110](https://github.com/linode/manager/pull/10110)) +- Improve User Profile integration test coverage and separate from Display Settings coverage ([#10202](https://github.com/linode/manager/pull/10202)) +- Add test for OBJ Multicluster bucket create flow ([#10211](https://github.com/linode/manager/pull/10211)) +- Suppress Rollup warnings during Cypress tests ([#10239](https://github.com/linode/manager/pull/10239)) + +### Upcoming Features: + +- Add list view for Linode Clone and Create from Backup ([#10182](https://github.com/linode/manager/pull/10182)) +- Add ‘Delete Placement Group’ Modal (#10162) +- Update Placement Groups types, methods and factories (#10200) +- Add placement group details to Create Linode payload ([#10195](https://github.com/linode/manager/pull/10195)) +- Update OBJ Multi-Cluster copy ([#10188](https://github.com/linode/manager/pull/10188)) +- Handle errors gracefully when OBJ Multi-Cluster feature flag is enabled without MSW (#10189) +- Ensure correct ARIA labels for permissions are displayed in Access Key "Permissions" drawer when OBJ Multicluster is enabled ([#10213](https://github.com/linode/manager/pull/10213)) +- Update Region Select for edge sites (#10194) +- Tag custom analytics events for account switching ([#10190](https://github.com/linode/manager/pull/10190)) +- Improve Billing & Account restricted user experience ([#10201](https://github.com/linode/manager/pull/10201)) +- Disable ability to edit or delete a proxy user via User Profile page ([#10202](https://github.com/linode/manager/pull/10202)) +- Fix Users & Grants filtering error based on `user_type` ([#10230](https://github.com/linode/manager/pull/10230)) +- Fix Account Switching ([#10234](https://github.com/linode/manager/pull/10234)) +- Fix to ensure ChildAccountList receives proper account token (#10234) +- Rename database scale up to database resize ([#10193](https://github.com/linode/manager/pull/10193)) + ## [2024-02-20] - v1.113.0 ### Added: diff --git a/packages/manager/cypress/e2e/core/account/account-login-history.spec.ts b/packages/manager/cypress/e2e/core/account/account-login-history.spec.ts index 6b213f48bf4..c06eb3c2959 100644 --- a/packages/manager/cypress/e2e/core/account/account-login-history.spec.ts +++ b/packages/manager/cypress/e2e/core/account/account-login-history.spec.ts @@ -130,7 +130,7 @@ describe('Account login history', () => { cy.findByLabelText('Account Logins').should('not.exist'); cy.findByText( - 'Access restricted. Please contact your business partner to request the necessary permission.' + "You don't have permissions to edit this Account. Please contact your business partner to request the necessary permissions." ); }); @@ -165,7 +165,7 @@ describe('Account login history', () => { cy.findByLabelText('Account Logins').should('not.exist'); cy.findByText( - 'Access restricted. Please contact your account administrator to request the necessary permission.' + "You don't have permissions to edit this Account. Please contact your account administrator to request the necessary permissions." ); }); }); diff --git a/packages/manager/cypress/e2e/core/account/change-username.spec.ts b/packages/manager/cypress/e2e/core/account/display-settings.spec.ts similarity index 73% rename from packages/manager/cypress/e2e/core/account/change-username.spec.ts rename to packages/manager/cypress/e2e/core/account/display-settings.spec.ts index a0374fc698f..a08f8e04b94 100644 --- a/packages/manager/cypress/e2e/core/account/change-username.spec.ts +++ b/packages/manager/cypress/e2e/core/account/display-settings.spec.ts @@ -8,10 +8,7 @@ import { makeFeatureFlagData } from 'support/util/feature-flags'; import { mockGetProfile } from 'support/intercepts/profile'; import { getProfile } from 'support/api/account'; import { interceptGetProfile } from 'support/intercepts/profile'; -import { - interceptGetUser, - mockUpdateUsername, -} from 'support/intercepts/account'; +import { mockUpdateUsername } from 'support/intercepts/account'; import { ui } from 'support/ui'; import { randomString } from 'support/util/random'; @@ -59,54 +56,7 @@ const verifyUsernameAndEmail = ( } }; -describe('username', () => { - /* - * - Validates username update flow via the user profile page using mocked data. - */ - it('can change username via user profile page', () => { - const newUsername = randomString(12); - - getProfile().then((profile) => { - const username = profile.body.username; - - interceptGetUser(username).as('getUser'); - mockUpdateUsername(username, newUsername).as('updateUsername'); - - cy.visitWithLogin(`account/users/${username}`); - cy.wait('@getUser'); - - cy.findByText('Username').should('be.visible'); - cy.findByText('Email').should('be.visible'); - cy.findByText('Delete User').should('be.visible'); - - cy.get('[id="username"]') - .should('be.visible') - .should('have.value', username) - .clear() - .type(newUsername); - - cy.get('[data-qa-textfield-label="Username"]') - .parent() - .parent() - .parent() - .within(() => { - ui.button - .findByTitle('Save') - .should('be.visible') - .should('be.enabled') - .click(); - }); - - cy.wait('@updateUsername'); - - // No confirmation gets shown on this page when changes are saved. - // Confirm that the text field has the correct value instead. - cy.get('[id="username"]') - .should('be.visible') - .should('have.value', newUsername); - }); - }); - +describe('Display Settings', () => { /* * - Validates username update flow via the profile display page using mocked data. */ diff --git a/packages/manager/cypress/e2e/core/account/user-profile.spec.ts b/packages/manager/cypress/e2e/core/account/user-profile.spec.ts new file mode 100644 index 00000000000..a7ffc031bc6 --- /dev/null +++ b/packages/manager/cypress/e2e/core/account/user-profile.spec.ts @@ -0,0 +1,283 @@ +import { accountUserFactory } from 'src/factories/accountUsers'; +import { getProfile } from 'support/api/account'; +import { + interceptGetUser, + mockGetUser, + mockGetUsers, + mockUpdateUsername, +} from 'support/intercepts/account'; +import { randomString } from 'support/util/random'; +import { ui } from 'support/ui'; +import { mockUpdateProfile } from 'support/intercepts/profile'; + +describe('User Profile', () => { + /* + * - Validates the flow of updating the username and email of the active account user via the User Profile page using mocked data. + */ + it('can change email and username of the active account', () => { + const newUsername = randomString(12); + const newEmail = `${newUsername}@example.com`; + + getProfile().then((profile) => { + const activeUsername = profile.body.username; + const activeEmail = profile.body.email; + + interceptGetUser(activeUsername).as('getUser'); + mockUpdateUsername(activeUsername, newUsername).as('updateUsername'); + mockUpdateProfile({ + ...profile.body, + email: newEmail, + }).as('updateEmail'); + + cy.visitWithLogin(`account/users/${activeUsername}`); + cy.wait('@getUser'); + + cy.findByText('Username').should('be.visible'); + cy.findByText('Email').should('be.visible'); + cy.findByText('Delete User').should('be.visible'); + + // Confirm the currently active user cannot be deleted. + ui.button + .findByTitle('Delete') + .should('be.visible') + .should('be.disabled') + .trigger('mouseover'); + // Click the button first, then confirm the tooltip is shown. + ui.tooltip + .findByText('You can\u{2019}t delete the currently active user.') + .should('be.visible'); + + // Confirm user can update their email before updating the username, since you cannot update a different user's (as determined by username) email. + cy.get('[id="email"]') + .should('be.visible') + .should('have.value', activeEmail) + .clear() + .type(newEmail); + + cy.get('[data-qa-textfield-label="Email"]') + .parent() + .parent() + .parent() + .within(() => { + ui.button + .findByTitle('Save') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait('@updateEmail'); + + // Confirm success notice displays. + cy.findByText('Email updated successfully').should('be.visible'); + + // Confirm user can update their username. + cy.get('[id="username"]') + .should('be.visible') + .should('have.value', activeUsername) + .clear() + .type(newUsername); + + cy.get('[data-qa-textfield-label="Username"]') + .parent() + .parent() + .parent() + .within(() => { + ui.button + .findByTitle('Save') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait('@updateUsername'); + + // No confirmation gets shown on this page when changes are saved. + // Confirm that the text field has the correct value instead. + cy.get('[id="username"]') + .should('be.visible') + .should('have.value', newUsername); + }); + }); + + /* + * - Validates the flow of updating the username and email of another user via the User Profile page using mocked data. + */ + it('can change the username but not email of another user account', () => { + const newUsername = randomString(12); + + getProfile().then((profile) => { + const additionalUsername = 'mock_user2'; + const mockAccountUsers = accountUserFactory.buildList(1, { + username: additionalUsername, + }); + const additionalUser = mockAccountUsers[0]; + + mockGetUsers(mockAccountUsers).as('getUsers'); + mockGetUser(additionalUser).as('getUser'); + mockUpdateUsername(additionalUsername, newUsername).as('updateUsername'); + + cy.visitWithLogin(`account/users/${additionalUsername}`); + + cy.wait('@getUser'); + + cy.findByText('Username').should('be.visible'); + cy.findByText('Email').should('be.visible'); + cy.findByText('Delete User').should('be.visible'); + ui.button.findByTitle('Delete').should('be.visible').should('be.enabled'); + + // Confirm email of another user cannot be updated. + cy.get('[id="email"]') + .should('be.visible') + .should('have.value', additionalUser.email) + .should('be.disabled') + .parent() + .parent() + .parent() + .within(() => { + ui.button + .findByAttribute('data-qa-help-button', 'true') + .should('be.visible') + .trigger('mouseover'); + // Click the button first, then confirm the tooltip is shown. + ui.tooltip + .findByText( + 'You can\u{2019}t change another user\u{2019}s email address.' + ) + .should('be.visible'); + }); + + cy.get('[data-qa-textfield-label="Email"]') + .parent() + .parent() + .parent() + .within(() => { + ui.button + .findByTitle('Save') + .should('be.visible') + .should('be.disabled') + .click(); + }); + + // Confirm username of another user can be updated. + cy.get('[id="username"]') + .should('be.visible') + .should('have.value', additionalUsername) + .clear() + .type(newUsername); + + cy.get('[data-qa-textfield-label="Username"]') + .parent() + .parent() + .parent() + .within(() => { + ui.button + .findByTitle('Save') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait('@updateUsername'); + + // No confirmation gets shown on this page when changes are saved. + // Confirm that the text field has the correct value instead. + cy.get('[id="username"]') + .should('be.visible') + .should('have.value', newUsername); + }); + }); + + /* + * - Validates disabled username and email flow for a proxy user via the User Profile page using mocked data. + */ + it('cannot change username or email for a proxy user or delete the proxy user', () => { + getProfile().then((profile) => { + const proxyUsername = 'proxy_user'; + const mockAccountUsers = accountUserFactory.buildList(1, { + username: proxyUsername, + user_type: 'proxy', + }); + + mockGetUsers(mockAccountUsers).as('getUsers'); + mockGetUser(mockAccountUsers[0]).as('getUser'); + + cy.visitWithLogin(`account/users/${proxyUsername}`); + + cy.wait('@getUser'); + + cy.findByText('Username').should('be.visible'); + cy.findByText('Email').should('be.visible'); + cy.findByText('Delete User').should('be.visible'); + + cy.get('[id="username"]') + .should('be.visible') + .should('have.value', proxyUsername) + .should('be.disabled') + .parent() + .parent() + .parent() + .within(() => { + ui.button + .findByAttribute('data-qa-help-button', 'true') + .should('be.visible') + .trigger('mouseover'); + // Click the button first, then confirm the tooltip is shown. + ui.tooltip + .findByText('This account type cannot update this field.') + .should('be.visible'); + }); + + cy.get('[data-qa-textfield-label="Username"]') + .parent() + .parent() + .parent() + .within(() => { + ui.button + .findByTitle('Save') + .should('be.visible') + .should('be.disabled'); + }); + + cy.get('[id="email"]') + .should('be.visible') + .should('be.disabled') + .parent() + .parent() + .parent() + .within(() => { + ui.button + .findByAttribute('data-qa-help-button', 'true') + .should('be.visible') + .trigger('mouseover'); + // Click the button first, then confirm the tooltip is shown. + ui.tooltip + .findByText('This account type cannot update this field.') + .should('be.visible'); + }); + + cy.get('[data-qa-textfield-label="Email"]') + .parent() + .parent() + .parent() + .within(() => { + ui.button + .findByTitle('Save') + .should('be.visible') + .should('be.disabled') + .click(); + }); + + // Confirms the proxy user cannot be deleted. + ui.button + .findByTitle('Delete') + .should('be.visible') + .should('be.disabled') + .trigger('mouseover'); + // Click the button first, then confirm the tooltip is shown. + ui.tooltip + .findByText('You can\u{2019}t delete a business partner user.') + .should('be.visible'); + }); + }); +}); diff --git a/packages/manager/cypress/e2e/core/billing/billing-contact.spec.ts b/packages/manager/cypress/e2e/core/billing/billing-contact.spec.ts index 0db765463af..dbd4d28727f 100644 --- a/packages/manager/cypress/e2e/core/billing/billing-contact.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/billing-contact.spec.ts @@ -2,6 +2,16 @@ import { mockGetAccount, mockUpdateAccount } from 'support/intercepts/account'; import { accountFactory } from 'src/factories/account'; import type { Account } from '@linode/api-v4'; import { ui } from 'support/ui'; +import { profileFactory } from '@src/factories'; + +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; + +import { mockGetProfile } from 'support/intercepts/profile'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; +import { randomLabel } from 'support/util/random'; /* eslint-disable sonarjs/no-duplicate-string */ const accountData = accountFactory.build({ @@ -151,3 +161,32 @@ describe('Billing Contact', () => { }); }); }); + +describe('Parent/Child feature disabled', () => { + beforeEach(() => { + mockAppendFeatureFlags({ + parentChildAccountAccess: makeFeatureFlagData(false), + }); + mockGetFeatureFlagClientstream(); + }); + + it('disables company name for Parent users', () => { + const mockProfile = profileFactory.build({ + username: randomLabel(), + restricted: false, + user_type: 'parent', + }); + + mockGetProfile(mockProfile); + cy.visitWithLogin('/account/billing/edit'); + + ui.drawer + .findByTitle('Edit Billing Contact Info') + .should('be.visible') + .within(() => { + cy.findByLabelText('Company Name') + .should('be.visible') + .should('be.disabled'); + }); + }); +}); diff --git a/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts b/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts index 8c6594b7f46..c0c349545b2 100644 --- a/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts @@ -18,13 +18,10 @@ import { ui } from 'support/ui'; import { makeFeatureFlagData } from 'support/util/feature-flags'; import { randomLabel } from 'support/util/random'; -// Tooltip message that appears on disabled billing action buttons for restricted users. +// Tooltip message that appears on disabled billing action buttons for restricted +// and child users. const restrictedUserTooltip = - 'To modify this content, please contact your administrator.'; - -// Tooltip message that appears on disabled billing action buttons for child users. -const childUserTooltip = - 'To modify this content, please contact your business partner.'; + "You don't have permissions to edit this Account."; // Mock credit card payment method to use in tests. const mockPaymentMethods = [ @@ -288,8 +285,8 @@ describe('restricted user billing flows', () => { mockGetUser(mockUser); cy.visitWithLogin('/account/billing'); - assertEditBillingInfoDisabled(childUserTooltip); - assertAddPaymentMethodDisabled(childUserTooltip); + assertEditBillingInfoDisabled(restrictedUserTooltip); + assertAddPaymentMethodDisabled(restrictedUserTooltip); }); /* diff --git a/packages/manager/cypress/e2e/core/objectStorage/access-keys.smoke.spec.ts b/packages/manager/cypress/e2e/core/objectStorage/access-keys.smoke.spec.ts index cc1f74b33b7..56383377eb5 100644 --- a/packages/manager/cypress/e2e/core/objectStorage/access-keys.smoke.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorage/access-keys.smoke.spec.ts @@ -394,7 +394,7 @@ describe('object storage access keys smoke tests', () => { mockBuckets.forEach((mockBucket) => { // TODO M3-7733 Update this selector when ARIA label is fixed. cy.findByLabelText( - `This token has read-only access for -${mockBucket.label}` + `This token has read-only access for ${mockRegion.id}-${mockBucket.label}` ); }); }); diff --git a/packages/manager/cypress/e2e/core/objectStorage/object-storage.smoke.spec.ts b/packages/manager/cypress/e2e/core/objectStorage/object-storage.smoke.spec.ts index e08a6fb50e1..8974866fb6b 100644 --- a/packages/manager/cypress/e2e/core/objectStorage/object-storage.smoke.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorage/object-storage.smoke.spec.ts @@ -4,6 +4,7 @@ import 'cypress-file-upload'; import { objectStorageBucketFactory } from 'src/factories/objectStorage'; +import { mockGetRegions } from 'support/intercepts/regions'; import { mockCreateBucket, mockDeleteBucket, @@ -13,11 +14,137 @@ import { mockGetBucketObjects, mockUploadBucketObject, mockUploadBucketObjectS3, + mockCreateBucketError, } from 'support/intercepts/object-storage'; -import { randomLabel } from 'support/util/random'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; +import { randomLabel, randomString } from 'support/util/random'; import { ui } from 'support/ui'; +import { regionFactory } from 'src/factories'; describe('object storage smoke tests', () => { + /* + * - Tests Object Storage bucket creation flow when OBJ Multicluster is enabled. + * - Confirms that expected regions are displayed in drop-down. + * - Confirms that region can be selected during create. + * - Confirms that API errors are handled gracefully by drawer. + * - Confirms that request payload contains desired Bucket region and not cluster. + * - Confirms that created Bucket is listed on the landing page. + */ + it('can create object storage bucket with OBJ Multicluster', () => { + const mockErrorMessage = 'An unknown error has occurred.'; + + const mockRegionWithObj = regionFactory.build({ + label: randomLabel(), + id: `${randomString(2)}-${randomString(3)}`, + capabilities: ['Object Storage'], + }); + + const mockRegionsWithoutObj = regionFactory.buildList(2, { + capabilities: [], + }); + + const mockRegions = [mockRegionWithObj, ...mockRegionsWithoutObj]; + + const mockBucket = objectStorageBucketFactory.build({ + label: randomLabel(), + region: mockRegionWithObj.id, + cluster: undefined, + objects: 0, + }); + + mockAppendFeatureFlags({ + objMultiCluster: makeFeatureFlagData(true), + }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientStream'); + + mockGetRegions(mockRegions).as('getRegions'); + mockGetBuckets([]).as('getBuckets'); + mockCreateBucketError(mockErrorMessage).as('createBucket'); + + cy.visitWithLogin('/object-storage'); + cy.wait(['@getRegions', '@getBuckets']); + + ui.button + .findByTitle('Create Bucket') + .should('be.visible') + .should('be.enabled') + .click(); + + ui.drawer + .findByTitle('Create Bucket') + .should('be.visible') + .within(() => { + // Submit button is disabled when fields are empty. + ui.buttonGroup + .findButtonByTitle('Create Bucket') + .should('be.visible') + .should('be.disabled'); + + // Enter label. + cy.contains('Label').click().type(mockBucket.label); + + cy.contains('Region').click().type(mockRegionWithObj.label); + + ui.autocompletePopper + .find() + .should('be.visible') + .within(() => { + // Confirm that regions without 'Object Storage' capability are not listed. + mockRegionsWithoutObj.forEach((mockRegionWithoutObj) => { + cy.contains(mockRegionWithoutObj.id).should('not.exist'); + }); + + // Confirm that region with 'Object Storage' capability is listed, + // then select it. + cy.findByText( + `${mockRegionWithObj.label} (${mockRegionWithObj.id})` + ) + .should('be.visible') + .click(); + }); + + // Close region select. + cy.contains('Region').click(); + + // On first attempt, mock an error response and confirm message is shown. + ui.buttonGroup + .findButtonByTitle('Create Bucket') + .should('be.visible') + .click(); + + cy.wait('@createBucket'); + cy.findByText(mockErrorMessage).should('be.visible'); + + // Click submit again, mock a successful response. + mockCreateBucket(mockBucket).as('createBucket'); + ui.buttonGroup + .findButtonByTitle('Create Bucket') + .should('be.visible') + .click(); + }); + + // Confirm that Cloud includes the "region" property and omits the "cluster" + // property in its payload when creating a bucket. + cy.wait('@createBucket').then((xhr) => { + const body = xhr.request.body; + expect(body.cluster).to.be.undefined; + expect(body.region).to.eq(mockRegionWithObj.id); + }); + + cy.findByText(mockBucket.label) + .should('be.visible') + .closest('tr') + .within(() => { + // TODO Confirm that bucket region is shown in landing page. + cy.findByText(mockBucket.hostname).should('be.visible'); + // cy.findByText(mockRegionWithObj.label).should('be.visible'); + }); + }); + /* * - Tests core object storage bucket create flow using mocked API responses. * - Creates bucket. @@ -29,8 +156,20 @@ describe('object storage smoke tests', () => { const bucketCluster = 'us-southeast-1'; const bucketHostname = `${bucketLabel}.${bucketCluster}.linodeobjects.com`; + const mockBucket = objectStorageBucketFactory.build({ + label: bucketLabel, + cluster: bucketCluster, + hostname: bucketHostname, + }); + + mockAppendFeatureFlags({ + objMultiCluster: makeFeatureFlagData(false), + }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientStream'); + mockGetBuckets([]).as('getBuckets'); - mockCreateBucket(bucketLabel, bucketCluster).as('createBucket'); + + mockCreateBucket(mockBucket).as('createBucket'); cy.visitWithLogin('/object-storage'); cy.wait('@getBuckets'); diff --git a/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts b/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts new file mode 100644 index 00000000000..52b053af660 --- /dev/null +++ b/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts @@ -0,0 +1,300 @@ +import { + accountFactory, + appTokenFactory, + profileFactory, +} from '@src/factories'; +import { accountUserFactory } from '@src/factories/accountUsers'; +import { DateTime } from 'luxon'; +import { + mockCreateChildAccountToken, + mockCreateChildAccountTokenError, + mockGetAccount, + mockGetChildAccounts, + mockGetChildAccountsError, + mockGetUser, +} from 'support/intercepts/account'; +import { mockGetEvents, mockGetNotifications } from 'support/intercepts/events'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { mockAllApiRequests } from 'support/intercepts/general'; +import { mockGetLinodes } from 'support/intercepts/linodes'; +import { mockGetProfile } from 'support/intercepts/profile'; +import { mockGetRegions } from 'support/intercepts/regions'; +import { ui } from 'support/ui'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; +import { assertLocalStorageValue } from 'support/util/local-storage'; +import { randomLabel, randomNumber, randomString } from 'support/util/random'; + +/** + * Confirms expected username and company name are shown in user menu button and yields the button. + * + * @param username - Username to expect in user menu button. + * @param companyName - Company name to expect in user menu button. + * + * @returns Cypress chainable that yields the user menu button. + */ +const assertUserMenuButton = (username: string, companyName: string) => { + return ui.userMenuButton + .find() + .should('be.visible') + .within(() => { + cy.findByText(username).should('be.visible'); + cy.findByText(companyName).should('be.visible'); + }); +}; + +/** + * Confirms that expected authentication values are set in Local Storage. + * + * @param token - Authentication token value to assert. + * @param expiry - Authentication expiry value to assert. + * @param scopes - Authentication scope value to assert. + */ +const assertAuthLocalStorage = ( + token: string, + expiry: string, + scopes: string +) => { + assertLocalStorageValue('authentication/token', `Bearer ${token}`); + assertLocalStorageValue('authentication/expire', expiry); + assertLocalStorageValue('authentication/scopes', scopes); +}; + +const mockParentAccount = accountFactory.build({ + company: 'Parent Company', +}); + +const mockParentProfile = profileFactory.build({ + username: randomLabel(), + user_type: 'parent', +}); + +const mockParentUser = accountUserFactory.build({ + username: mockParentProfile.username, + user_type: 'parent', +}); + +const mockChildAccount = accountFactory.build({ + company: 'Child Company', +}); + +const mockChildAccountToken = appTokenFactory.build({ + id: randomNumber(), + created: DateTime.now().toISO(), + expiry: DateTime.now().plus({ hours: 1 }).toISO(), + label: `${mockChildAccount.company}_proxy`, + scopes: '*', + token: randomString(32), + website: undefined, + thumbnail_url: undefined, +}); + +const mockErrorMessage = 'An unknown error has occurred.'; + +describe('Parent/Child account switching', () => { + /* + * Tests to confirm that Parent account users can switch to Child accounts as expected. + */ + describe('From Parent to Proxy', () => { + beforeEach(() => { + // @TODO M3-7554, M3-7559: Remove feature flag mocks after feature launch and clean-up. + mockAppendFeatureFlags({ + parentChildAccountAccess: makeFeatureFlagData(true), + }); + mockGetFeatureFlagClientstream(); + }); + + /* + * - Confirms that Parent account user can switch to Child account from Account Billing page. + * - Confirms that Child account information is displayed in user menu button after switch. + * - Confirms that Cloud updates local storage auth values upon account switch. + */ + it('can switch from Parent account to Child account from Billing page', () => { + mockGetProfile(mockParentProfile); + mockGetAccount(mockParentAccount); + mockGetChildAccounts([mockChildAccount]); + mockGetUser(mockParentUser); + + cy.visitWithLogin('/account/billing'); + + // Confirm that "Switch Account" button is present, then click it. + ui.button + .findByTitle('Switch Account') + .should('be.visible') + .should('be.enabled') + .click(); + + // Prepare up mocks in advance of the account switch. As soon as the child account is clicked, + // Cloud will replace its stored token with the token provided by the API and then reload. + // From that point forward, we will not have a valid test account token stored in local storage, + // so all non-intercepted API requests will respond with a 401 status code and we will get booted to login. + // We'll mitigate this by broadly mocking ALL API-v4 requests, then applying more specific mocks to the + // individual requests as needed. + mockAllApiRequests(); + mockGetLinodes([]); + mockGetRegions([]); + mockGetEvents([]); + mockGetNotifications([]); + mockGetAccount(mockChildAccount); + mockGetProfile(mockParentProfile); + mockGetUser(mockParentUser); + + // Mock the account switch itself -- we have to do this after the mocks above + // to ensure that it is applied. + mockCreateChildAccountToken(mockChildAccount, mockChildAccountToken).as( + 'switchAccount' + ); + + ui.drawer + .findByTitle('Switch Account') + .should('be.visible') + .within(() => { + cy.findByText(mockChildAccount.company).should('be.visible').click(); + }); + + cy.wait('@switchAccount'); + + // Confirm that Cloud Manager updates local storage authentication values. + // Satisfy TypeScript using non-null assertions since we know what the mock data contains. + assertAuthLocalStorage( + mockChildAccountToken.token!, + mockChildAccountToken.expiry!, + mockChildAccountToken.scopes + ); + + // Confirm expected username and company are shown in user menu button. + assertUserMenuButton( + mockParentProfile.username, + mockChildAccount.company + ); + }); + + /* + * - Confirms that Parent account user can switch to Child account using the user menu. + * - Confirms that Parent account information is initially displayed in user menu button. + * - Confirms that Child account information is displayed in user menu button after switch. + * - Confirms that Cloud updates local storage auth values upon account switch. + */ + it('can switch from Parent account to Child account using user menu', () => { + mockGetProfile(mockParentProfile); + mockGetAccount(mockParentAccount); + mockGetChildAccounts([mockChildAccount]); + mockGetUser(mockParentUser); + + cy.visitWithLogin('/'); + + // Confirm that Parent account username and company name are shown in user + // menu button, then click the button. + assertUserMenuButton( + mockParentProfile.username, + mockParentAccount.company + ).click(); + + // Click "Switch Account" button in user menu. + ui.userMenu + .find() + .should('be.visible') + .within(() => { + ui.button + .findByTitle('Switch Account') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + // Prepare up mocks in advance of the account switch. As soon as the child account is clicked, + // Cloud will replace its stored token with the token provided by the API and then reload. + // From that point forward, we will not have a valid test account token stored in local storage, + // so all non-intercepted API requests will respond with a 401 status code and we will get booted to login. + // We'll mitigate this by broadly mocking ALL API-v4 requests, then applying more specific mocks to the + // individual requests as needed. + mockAllApiRequests(); + mockGetLinodes([]); + mockGetRegions([]); + mockGetEvents([]); + mockGetNotifications([]); + mockGetAccount(mockChildAccount); + mockGetProfile(mockParentProfile); + mockGetUser(mockParentUser); + + // Click mock company name in "Switch Account" drawer. + mockCreateChildAccountToken(mockChildAccount, mockChildAccountToken).as( + 'switchAccount' + ); + + ui.drawer + .findByTitle('Switch Account') + .should('be.visible') + .within(() => { + cy.findByText(mockChildAccount.company).should('be.visible').click(); + }); + + cy.wait('@switchAccount'); + + // Confirm that Cloud Manager updates local storage authentication values. + // Satisfy TypeScript using non-null assertions since we know what the mock data contains. + assertAuthLocalStorage( + mockChildAccountToken.token!, + mockChildAccountToken.expiry!, + mockChildAccountToken.scopes + ); + + // Confirm expected username and company are shown in user menu button. + assertUserMenuButton( + mockParentProfile.username, + mockChildAccount.company + ); + }); + }); + + /* + * Tests to confirm that Cloud handles account switching errors gracefully. + */ + describe('Error flows', () => { + /* + * - Confirms error handling upon failure to fetch child accounts. + * - Confirms "Try Again" button can be used to re-fetch child accounts successfully. + * - Confirms error handling upon failure to create child account token. + */ + it('handles account switching API errors', () => { + mockGetProfile(mockParentProfile); + mockGetAccount(mockParentAccount); + mockGetChildAccountsError('An unknown error has occurred', 500); + mockGetUser(mockParentUser); + + cy.visitWithLogin('/account/billing'); + ui.button + .findByTitle('Switch Account') + .should('be.visible') + .should('be.enabled') + .click(); + + ui.drawer + .findByTitle('Switch Account') + .should('be.visible') + .within(() => { + // Confirm error message upon failure to fetch child accounts. + cy.findByText('Unable to load data.').should('be.visible'); + cy.findByText( + 'Try again or contact support if the issue persists.' + ).should('be.visible'); + + // Click "Try Again" button and mock a successful response. + mockGetChildAccounts([mockChildAccount]); + ui.button + .findByTitle('Try again') + .should('be.visible') + .should('be.enabled') + .click(); + + // Click child company and mock an error. + // Confirm that Cloud Manager displays the error message in the drawer. + mockCreateChildAccountTokenError(mockChildAccount, mockErrorMessage); + cy.findByText(mockChildAccount.company).click(); + cy.findByText(mockErrorMessage).should('be.visible'); + }); + }); + }); +}); diff --git a/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts new file mode 100644 index 00000000000..59436b4e983 --- /dev/null +++ b/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts @@ -0,0 +1,270 @@ +import { + eventFactory, + linodeFactory, + notificationFactory, + volumeFactory, +} from '@src/factories'; +import { mockGetEvents, mockGetNotifications } from 'support/intercepts/events'; +import { + mockGetLinodeDetails, + mockGetLinodeDisks, + mockGetLinodeVolumes, +} from 'support/intercepts/linodes'; +import { mockMigrateVolumes, mockGetVolumes } from 'support/intercepts/volumes'; +import { ui } from 'support/ui'; + +describe('volume upgrade/migration', () => { + it('can upgrade an unattached volume to NVMe', () => { + const volume = volumeFactory.build(); + + const migrationScheduledNotification = notificationFactory.build({ + type: 'volume_migration_scheduled', + entity: { type: 'volume', id: volume.id }, + }); + + mockGetVolumes([volume]).as('getVolumes'); + mockMigrateVolumes().as('migrateVolumes'); + mockGetNotifications([migrationScheduledNotification]).as( + 'getNotifications' + ); + + cy.visitWithLogin('/volumes'); + + cy.wait(['@getVolumes', '@getNotifications']); + + cy.findByText('UPGRADE TO NVMe') + .should('be.visible') + .should('be.enabled') + .click(); + + const migrationImminentNotification = notificationFactory.build({ + type: 'volume_migration_imminent', + entity: { type: 'volume', id: volume.id }, + }); + mockGetNotifications([migrationImminentNotification]).as( + 'getNotifications' + ); + + ui.dialog.findByTitle(`Upgrade Volume ${volume.label}`).within(() => { + ui.button + .findByTitle('Enter Upgrade Queue') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait(['@migrateVolumes', '@getNotifications']); + + cy.findByText('UPGRADE PENDING').should('be.visible'); + + for (const percentage of [10, 20, 50, 75]) { + const mockStartedMigrationEvent = eventFactory.build({ + action: 'volume_migrate', + entity: { id: volume.id, type: 'volume' }, + status: 'started', + percent_complete: percentage, + }); + + mockGetEvents([mockStartedMigrationEvent]).as('getEvents'); + + cy.wait('@getEvents'); + + cy.findByText(`migrating (${percentage}%)`).should('be.visible'); + } + + const mockFinishedMigrationEvent = eventFactory.build({ + action: 'volume_migrate', + entity: { id: volume.id, type: 'volume', label: volume.label }, + status: 'finished', + }); + + mockGetEvents([mockFinishedMigrationEvent]).as('getEvents'); + mockGetNotifications([]).as('getNotifications'); + + cy.wait(['@getEvents', '@getVolumes', '@getNotifications']); + + mockGetEvents([]); + + cy.findByText('active').should('be.visible'); + + ui.toast.assertMessage(`Volume ${volume.label} successfully upgraded.`); + }); + + it('can upgrade an attached volume from the volumes landing page', () => { + const linode = linodeFactory.build(); + const volume = volumeFactory.build({ + linode_id: linode.id, + linode_label: linode.label, + }); + + const migrationScheduledNotification = notificationFactory.build({ + type: 'volume_migration_scheduled', + entity: { type: 'volume', id: volume.id }, + }); + + mockGetVolumes([volume]).as('getVolumes'); + mockMigrateVolumes().as('migrateVolumes'); + mockGetLinodeDetails(linode.id, linode).as('getLinode'); + mockGetLinodeDisks(linode.id, []); + mockGetNotifications([migrationScheduledNotification]).as( + 'getNotifications' + ); + mockGetLinodeVolumes(linode.id, [volume]).as('getLinodeVolumes'); + + cy.visitWithLogin('/volumes'); + + cy.wait(['@getVolumes', '@getNotifications']); + + cy.findByText('UPGRADE TO NVMe') + .should('be.visible') + .should('be.enabled') + .click(); + + cy.url().should('contain', `/linodes/${linode.id}/storage?upgrade=true`); + + cy.wait(['@getLinode', '@getLinodeVolumes']); + + const migrationImminentNotification = notificationFactory.build({ + type: 'volume_migration_imminent', + entity: { type: 'volume', id: volume.id }, + }); + mockGetNotifications([migrationImminentNotification]).as( + 'getNotifications' + ); + + ui.dialog.findByTitle('Upgrade Volume').within(() => { + cy.findByText( + `A Volume attached to Linode ${linode.label} will be upgraded to high-performance NVMe Block Storage.`, + { exact: false } + ).should('be.visible'); + + ui.button + .findByTitle('Enter Upgrade Queue') + .should('be.visible') + .should('be.enabled') + .click(); + }); + cy.wait(['@migrateVolumes', '@getNotifications']); + + cy.findByText('UPGRADE PENDING').should('be.visible'); + + for (const percentage of [10, 20, 50, 75]) { + const mockStartedMigrationEvent = eventFactory.build({ + action: 'volume_migrate', + entity: { id: volume.id, type: 'volume' }, + status: 'started', + percent_complete: percentage, + }); + + mockGetEvents([mockStartedMigrationEvent]).as('getEvents'); + + cy.wait('@getEvents'); + + cy.findByText(`migrating (${percentage}%)`).should('be.visible'); + } + + const mockFinishedMigrationEvent = eventFactory.build({ + action: 'volume_migrate', + entity: { id: volume.id, type: 'volume', label: volume.label }, + status: 'finished', + }); + + mockGetEvents([mockFinishedMigrationEvent]).as('getEvents'); + mockGetNotifications([]).as('getNotifications'); + + cy.wait(['@getEvents', '@getLinodeVolumes', '@getNotifications']); + + mockGetEvents([]); + + cy.findByText('active').should('be.visible'); + + ui.toast.assertMessage(`Volume ${volume.label} successfully upgraded.`); + }); + + it('can upgrade an attached volume from the linode details page', () => { + const linode = linodeFactory.build(); + const volume = volumeFactory.build({ + linode_id: linode.id, + linode_label: linode.label, + }); + + const migrationScheduledNotification = notificationFactory.build({ + type: 'volume_migration_scheduled', + entity: { type: 'volume', id: volume.id }, + }); + + mockMigrateVolumes().as('migrateVolumes'); + mockGetLinodeDetails(linode.id, linode).as('getLinode'); + mockGetLinodeDisks(linode.id, []); + mockGetNotifications([migrationScheduledNotification]).as( + 'getNotifications' + ); + mockGetLinodeVolumes(linode.id, [volume]).as('getLinodeVolumes'); + + cy.visitWithLogin(`/linodes/${linode.id}/storage`); + + cy.wait(['@getLinode', '@getLinodeVolumes', '@getNotifications']); + + ui.button + .findByTitle('Upgrade Volume') + .should('be.visible') + .should('be.enabled') + .click(); + + const migrationImminentNotification = notificationFactory.build({ + type: 'volume_migration_imminent', + entity: { type: 'volume', id: volume.id }, + }); + mockGetNotifications([migrationImminentNotification]).as( + 'getNotifications' + ); + + ui.dialog.findByTitle('Upgrade Volume').within(() => { + cy.findByText( + `A Volume attached to Linode ${linode.label} will be upgraded to high-performance NVMe Block Storage.`, + { exact: false } + ).should('be.visible'); + + ui.button + .findByTitle('Enter Upgrade Queue') + .should('be.visible') + .should('be.enabled') + .click(); + }); + cy.wait(['@migrateVolumes', '@getNotifications']); + + cy.findByText('UPGRADE PENDING').should('be.visible'); + + for (const percentage of [10, 20, 50, 75]) { + const mockStartedMigrationEvent = eventFactory.build({ + action: 'volume_migrate', + entity: { id: volume.id, type: 'volume' }, + status: 'started', + percent_complete: percentage, + }); + + mockGetEvents([mockStartedMigrationEvent]).as('getEvents'); + + cy.wait('@getEvents'); + + cy.findByText(`migrating (${percentage}%)`).should('be.visible'); + } + + const mockFinishedMigrationEvent = eventFactory.build({ + action: 'volume_migrate', + entity: { id: volume.id, type: 'volume', label: volume.label }, + status: 'finished', + }); + + mockGetEvents([mockFinishedMigrationEvent]).as('getEvents'); + mockGetNotifications([]).as('getNotifications'); + + cy.wait(['@getEvents', '@getLinodeVolumes', '@getNotifications']); + + mockGetEvents([]); + + cy.findByText('active').should('be.visible'); + + ui.toast.assertMessage(`Volume ${volume.label} successfully upgraded.`); + }); +}); diff --git a/packages/manager/cypress/support/intercepts/account.ts b/packages/manager/cypress/support/intercepts/account.ts index 5eb29242169..27838a71068 100644 --- a/packages/manager/cypress/support/intercepts/account.ts +++ b/packages/manager/cypress/support/intercepts/account.ts @@ -20,6 +20,7 @@ import type { InvoiceItem, Payment, PaymentMethod, + Token, User, } from '@linode/api-v4'; @@ -503,6 +504,7 @@ export const mockCancelAccountError = ( /** * Intercepts GET request to fetch the account agreements and mocks the response. * + * @param agreements - Agreements with which to mock response. * * @returns Cypress chainable. */ @@ -516,9 +518,86 @@ export const mockGetAccountAgreements = ( ); }; +/** + * Intercepts GET request to fetch child accounts and mocks the response. + * + * @param childAccounts - Child account objects with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockGetChildAccounts = ( + childAccounts: Account[] +): Cypress.Chainable => { + return cy.intercept( + 'GET', + apiMatcher('account/child-accounts*'), + paginateResponse(childAccounts) + ); +}; + +/** + * Intercepts GET request to fetch child accounts and mocks an error response. + * + * @param errorMessage - API error message with which to mock response. + * @param statusCode - HTTP status code with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockGetChildAccountsError = ( + errorMessage: string = 'An unknown error has occurred', + statusCode: number = 500 +) => { + return cy.intercept( + 'GET', + apiMatcher('account/child-accounts*'), + makeErrorResponse(errorMessage, statusCode) + ); +}; + +/** + * Intercepts POST request to create a child account token and mocks the response. + * + * @param childAccount - Child account for which to create a token. + * @param childAccountToken - Token object with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockCreateChildAccountToken = ( + childAccount: Account, + childAccountToken: Token +): Cypress.Chainable => { + return cy.intercept( + 'POST', + apiMatcher(`account/child-accounts/${childAccount.euuid}/token`), + makeResponse(childAccountToken) + ); +}; + +/** + * Intercepts POST request to create a child account token and mocks error response. + * + * @param childAccount - Child account for which to mock error response. + * @param errorMessage - API error message with which to mock response. + * @param statusCode - HTTP status code with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockCreateChildAccountTokenError = ( + childAccount: Account, + errorMessage: string = 'An unknown error has occurred', + statusCode: number = 500 +): Cypress.Chainable => { + return cy.intercept( + 'POST', + apiMatcher(`account/child-accounts/${childAccount.euuid}/token`), + makeErrorResponse(errorMessage, statusCode) + ); +}; + /** * Intercepts GET request to fetch the account logins and mocks the response. * + * @param accountLogins - Account login objects with which to mock response. * * @returns Cypress chainable. */ diff --git a/packages/manager/cypress/support/intercepts/events.ts b/packages/manager/cypress/support/intercepts/events.ts index 2ae3685bd5e..eea7b5fac12 100644 --- a/packages/manager/cypress/support/intercepts/events.ts +++ b/packages/manager/cypress/support/intercepts/events.ts @@ -2,7 +2,7 @@ * @file Mocks and intercepts related to notification and event handling. */ -import { Event } from '@linode/api-v4'; +import type { Event, Notification } from '@linode/api-v4'; import { apiMatcher } from 'support/util/intercepts'; import { paginateResponse } from 'support/util/paginate'; @@ -20,3 +20,20 @@ export const mockGetEvents = (events: Event[]): Cypress.Chainable => { paginateResponse(events) ); }; + +/** + * Intercepts GET request to fetch notifications and mocks response. + * + * @param notifications - Notifications with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockGetNotifications = ( + notifications: Notification[] +): Cypress.Chainable => { + return cy.intercept( + 'GET', + apiMatcher('account/notifications*'), + paginateResponse(notifications) + ); +}; diff --git a/packages/manager/cypress/support/intercepts/general.ts b/packages/manager/cypress/support/intercepts/general.ts index 3cd13083bc5..09cd74e0167 100644 --- a/packages/manager/cypress/support/intercepts/general.ts +++ b/packages/manager/cypress/support/intercepts/general.ts @@ -2,6 +2,25 @@ import { makeErrorResponse } from 'support/util/errors'; import { apiMatcher } from 'support/util/intercepts'; import { makeResponse } from 'support/util/response'; +/** + * Intercepts all requests to Linode API v4 and mocks an HTTP response. + * + * This is useful to apply a baseline mock on all Linode API v4 requests, e.g. + * to prevent 401 responses. More fine-grained mocking can be set up with + * subsequent calls to other mock utils. + * + * @param body - Body data with which to mock response. + * @param statusCode - HTTP status code with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockAllApiRequests = ( + body: any = {}, + statusCode: number = 200 +) => { + return cy.intercept(apiMatcher('**/*'), makeResponse(body, statusCode)); +}; + /** * Intercepts GET request to given URL and mocks an HTTP 200 response with the given content. * diff --git a/packages/manager/cypress/support/intercepts/object-storage.ts b/packages/manager/cypress/support/intercepts/object-storage.ts index f993da79d73..8d13d437d8c 100644 --- a/packages/manager/cypress/support/intercepts/object-storage.ts +++ b/packages/manager/cypress/support/intercepts/object-storage.ts @@ -7,13 +7,12 @@ import { apiMatcher } from 'support/util/intercepts'; import { paginateResponse } from 'support/util/paginate'; import { makeResponse } from 'support/util/response'; -import { objectStorageBucketFactory } from 'src/factories/objectStorage'; - import type { ObjectStorageBucket, ObjectStorageKey, ObjectStorageCluster, } from '@linode/api-v4'; +import { makeErrorResponse } from 'support/util/errors'; /** * Intercepts GET requests to fetch buckets. @@ -80,25 +79,38 @@ export const interceptCreateBucket = (): Cypress.Chainable => { }; /** - * Intercepts POST request to create bucket and mocks response. + * Intercepts POST request to create a bucket and mocks response. * - * @param label - Object storage bucket label. - * @param cluster - Object storage bucket cluster. + * @param bucket - Bucket with which to mock response. * * @returns Cypress chainable. */ export const mockCreateBucket = ( - label: string, - cluster: string + bucket: ObjectStorageBucket +): Cypress.Chainable => { + return cy.intercept( + 'POST', + apiMatcher('object-storage/buckets'), + makeResponse(bucket) + ); +}; + +/** + * Intercepts POST request to create a bucket and mocks an error response. + * + * @param errorMessage - Optional error message with which to mock response. + * @param statusCode - HTTP status code with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockCreateBucketError = ( + errorMessage: string = 'An unknown error occurred.', + statusCode: number = 500 ): Cypress.Chainable => { return cy.intercept( 'POST', apiMatcher('object-storage/buckets'), - objectStorageBucketFactory.build({ - cluster, - hostname: `${label}.${cluster}.linodeobjects.com`, - label, - }) + makeErrorResponse(errorMessage, statusCode) ); }; diff --git a/packages/manager/cypress/support/intercepts/volumes.ts b/packages/manager/cypress/support/intercepts/volumes.ts index 622ba081a5a..536da877b34 100644 --- a/packages/manager/cypress/support/intercepts/volumes.ts +++ b/packages/manager/cypress/support/intercepts/volumes.ts @@ -113,3 +113,12 @@ export const interceptDeleteVolume = ( ): Cypress.Chainable => { return cy.intercept('DELETE', apiMatcher(`volumes/${volumeId}`)); }; + +/** + * Intercepts POST request to migrate volumes and mocks response. + * + * @returns Cypress chainable. + */ +export const mockMigrateVolumes = (): Cypress.Chainable => { + return cy.intercept('POST', apiMatcher(`volumes/migrate`), {}); +}; diff --git a/packages/manager/cypress/support/ui/autocomplete.ts b/packages/manager/cypress/support/ui/autocomplete.ts index 94e391232b1..e87e51f1f6f 100644 --- a/packages/manager/cypress/support/ui/autocomplete.ts +++ b/packages/manager/cypress/support/ui/autocomplete.ts @@ -19,7 +19,14 @@ export const autocomplete = { */ export const autocompletePopper = { /** - * Finds a autocomplete popper that has the given title. + * Finds an open autocomplete popper. + */ + find: () => { + return cy.document().its('body').find('[data-qa-autocomplete-popper]'); + }, + + /** + * Finds an item within an autocomplete popper that has the given title. */ findByTitle: ( title: string, diff --git a/packages/manager/cypress/support/ui/select.ts b/packages/manager/cypress/support/ui/select.ts index 6d117789272..d9aad65293d 100644 --- a/packages/manager/cypress/support/ui/select.ts +++ b/packages/manager/cypress/support/ui/select.ts @@ -25,6 +25,7 @@ export const select = { * @returns Cypress chainable. */ findItemById: (id: string) => { + // eslint-disable-next-line cypress/unsafe-to-chain-command return cy .get(`[data-qa-option="${id}"]`) .scrollIntoView() @@ -41,6 +42,7 @@ export const select = { * @returns Cypress chainable. */ findItemByText: (text: string) => { + // eslint-disable-next-line cypress/unsafe-to-chain-command return cy .get('[data-qa-option]') .contains(text) @@ -58,6 +60,7 @@ export const select = { * @returns Cypress chainable. */ findLinodeItemByText: (text: string) => { + // eslint-disable-next-line cypress/unsafe-to-chain-command return cy .get('[data-qa-linode-option]') .contains(text) diff --git a/packages/manager/cypress/support/util/local-storage.ts b/packages/manager/cypress/support/util/local-storage.ts new file mode 100644 index 00000000000..db748cb4105 --- /dev/null +++ b/packages/manager/cypress/support/util/local-storage.ts @@ -0,0 +1,30 @@ +/** + * @file Utilities to access and validate Local Storage data. + */ + +/** + * Asserts that a local storage item has a given value. + * + * @param key - Local storage item key. + * @param value - Local storage item value to assert. + */ +export const assertLocalStorageValue = (key: string, value: any) => { + cy.getAllLocalStorage().then((localStorageData: any) => { + const origin = Cypress.config('baseUrl'); + if (!origin) { + // This should never happen in practice. + throw new Error('Unable to retrieve Cypress base URL configuration'); + } + if (!localStorageData[origin]) { + throw new Error( + `Unable to retrieve local storage data from origin '${origin}'` + ); + } + if (!localStorageData[origin][key]) { + throw new Error( + `No local storage data exists for key '${key}' and origin '${origin}'` + ); + } + expect(localStorageData[origin][key]).equals(value); + }); +}; diff --git a/packages/manager/cypress/vite.config.ts b/packages/manager/cypress/vite.config.ts index 64101f8589e..1e25f3d86f9 100644 --- a/packages/manager/cypress/vite.config.ts +++ b/packages/manager/cypress/vite.config.ts @@ -1,3 +1,5 @@ +import react from '@vitejs/plugin-react-swc'; +import svgr from 'vite-plugin-svgr'; import { defineConfig } from 'vite'; import { URL } from 'url'; @@ -5,6 +7,30 @@ import { URL } from 'url'; const DIRNAME = new URL('.', import.meta.url).pathname; export default defineConfig({ + plugins: [react(), svgr({ exportAsDefault: true })], + build: { + rollupOptions: { + // Suppress "SOURCEMAP_ERROR" warnings. + // This is necessary because MUI contains React SSR "use client" module-level + // directive, and Rollup does not support module-level directives. + // `vite-plugin-react` and `vite-plugin-react-swc` both silence this warning, + // but the inability to handle module-level directives also causes Sourcemap + // output errors. The warnings for this are not suppressed by Vite plugins, + // so we need to suppress them ourselves. + // + // See also: + // - https://github.com/vitejs/vite-plugin-react/blob/7f53c63/packages/plugin-react/src/index.ts#L303 + // - https://github.com/vitejs/vite-plugin-react-swc/blob/53ecc44/src/index.ts#L262 + // - https://github.com/rollup/rollup/issues/4699#issuecomment-1299770973 + // - https://github.com/vitejs/vite/issues/15012#issuecomment-1815854072 + onwarn(warning, defaultHandler) { + if (warning.code === 'SOURCEMAP_ERROR') { + return; + } + defaultHandler(warning); + }, + }, + }, resolve: { alias: { '@src': `${DIRNAME}/../src`, diff --git a/packages/manager/package.json b/packages/manager/package.json index 1f86cacdb8d..bc5a2813a46 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -2,7 +2,7 @@ "name": "linode-manager", "author": "Linode", "description": "The Linode Manager website", - "version": "1.113.0", + "version": "1.114.0", "private": true, "type": "module", "bugs": { @@ -49,10 +49,10 @@ "patch-package": "^7.0.0", "qrcode.react": "^0.8.0", "ramda": "~0.25.0", - "react": "^17.0.2", + "react": "^18.2.0", "react-beautiful-dnd": "^13.0.0", "react-csv": "^2.0.3", - "react-dom": "^17.0.2", + "react-dom": "^18.2.0", "react-dropzone": "~11.2.0", "react-number-format": "^3.5.0", "react-query": "^3.3.2", @@ -83,7 +83,7 @@ "start:expose": "concurrently --raw \"vite --host\" \"tsc --watch --preserveWatchOutput\"", "start:ci": "yarn serve ./build -p 3000 -s --cors", "lint": "yarn run eslint . --ext .js,.ts,.tsx --quiet", - "build": "node scripts/prebuild.mjs && vite build", + "build": "vite build", "build:analyze": "bunx vite-bundle-visualizer", "precommit": "lint-staged && yarn typecheck", "test": "vitest run", @@ -107,31 +107,29 @@ }, "devDependencies": { "@linode/eslint-plugin-cloud-manager": "^0.0.3", - "@storybook/addon-actions": "^7.6.10", - "@storybook/addon-controls": "^7.6.10", - "@storybook/addon-docs": "^7.6.10", - "@storybook/addon-mdx-gfm": "^7.6.10", - "@storybook/addon-measure": "^7.6.10", - "@storybook/addon-storysource": "^7.6.10", - "@storybook/addon-viewport": "^7.6.10", - "@storybook/addons": "^7.6.10", - "@storybook/client-api": "^7.6.10", - "@storybook/react": "^7.6.10", - "@storybook/react-vite": "^7.6.10", - "@storybook/theming": "^7.6.10", + "@storybook/addon-actions": "^7.6.17", + "@storybook/addon-controls": "^7.6.17", + "@storybook/addon-docs": "^7.6.17", + "@storybook/addon-mdx-gfm": "^7.6.17", + "@storybook/addon-measure": "^7.6.17", + "@storybook/addon-storysource": "^7.6.17", + "@storybook/addon-viewport": "^7.6.17", + "@storybook/addons": "^7.6.17", + "@storybook/client-api": "^7.6.17", + "@storybook/react": "^7.6.17", + "@storybook/react-vite": "^7.6.17", + "@storybook/theming": "^7.6.17", "@swc/core": "^1.3.1", "@testing-library/cypress": "^10.0.0", - "@testing-library/jest-dom": "~5.11.3", - "@testing-library/react": "~10.4.9", - "@testing-library/react-hooks": "~3.4.1", - "@testing-library/user-event": "^12.1.1", + "@testing-library/jest-dom": "~6.4.2", + "@testing-library/react": "~14.2.1", + "@testing-library/user-event": "^14.5.2", "@types/braintree-web": "^3.75.23", "@types/chai-string": "^1.4.5", "@types/chart.js": "^2.9.21", "@types/css-mediaquery": "^0.1.1", "@types/he": "^1.1.0", "@types/highlight.js": "~10.1.0", - "@types/jest-axe": "^3.5.7", "@types/jsdom": "^21.1.4", "@types/jspdf": "^1.3.3", "@types/luxon": "3.4.2", @@ -141,10 +139,10 @@ "@types/node": "^12.7.1", "@types/qrcode.react": "^0.8.0", "@types/ramda": "0.25.16", - "@types/react": "^17.0.27", + "@types/react": "^18.2.55", "@types/react-beautiful-dnd": "^13.0.0", "@types/react-csv": "^1.1.3", - "@types/react-dom": "^17.0.9", + "@types/react-dom": "^18.2.18", "@types/react-redux": "~7.1.7", "@types/react-router-dom": "~5.3.3", "@types/react-router-hash-link": "^1.2.1", @@ -165,7 +163,7 @@ "chalk": "^5.2.0", "commander": "^6.2.1", "css-mediaquery": "^0.1.2", - "cypress": "^13.5.0", + "cypress": "13.5.0", "cypress-axe": "^1.0.0", "cypress-file-upload": "^5.0.7", "cypress-real-events": "^1.11.0", @@ -188,7 +186,6 @@ "eslint-plugin-xss": "^0.1.10", "factory.ts": "^0.5.1", "glob": "^10.3.1", - "jest-axe": "^8.0.0", "jsdom": "^22.1.0", "junit2json": "^3.1.4", "lint-staged": "^13.2.2", @@ -200,7 +197,7 @@ "reselect-tools": "^0.0.7", "serve": "^14.0.1", "simple-git": "^3.19.0", - "storybook": "^7.6.10", + "storybook": "^7.6.17", "storybook-dark-mode": "^3.0.3", "ts-node": "^10.9.2", "vite": "^5.0.12", diff --git a/packages/manager/scripts/prebuild.mjs b/packages/manager/scripts/prebuild.mjs deleted file mode 100644 index eb5461cf866..00000000000 --- a/packages/manager/scripts/prebuild.mjs +++ /dev/null @@ -1,89 +0,0 @@ -/** - * This Node script is run during our build process. - * These endpoints are extremely unlikely to change between - * Cloud releases, so by including these requests in our build - * pipeline and reading them as if they were hard coded from the app, - * we can prevent unnecessary network requests. - */ -import { writeFileSync } from 'fs'; - -// Always use prod API rather than the variable in /src/constants -const API_ROOT = 'https://api.linode.com/v4/'; -const DATA_DIR = 'src/cachedData/'; -const cachedRequests = [ - { - endpoint: 'regions', - filename: 'regions.json', - }, - { - endpoint: 'linode/types-legacy', - filename: 'typesLegacy.json', - }, - // Only used for testing purposes, never for displaying data to users - { - endpoint: 'linode/kernels', - filename: 'kernels.json', - }, - { - endpoint: 'linode/stackscripts', - filename: 'marketplace.json', - filter: [ - { - '+and': [ - { - '+or': [ - { username: 'linode-stackscripts' }, - { username: 'linode' }, - ], - }, - { - label: { - '+contains': 'One-Click', - }, - }, - ], - }, - { '+order_by': 'ordinal' }, - ], - }, -]; - -async function handleRequest(endpoint, filename, filter) { - const response = await fetch(API_ROOT + endpoint + '?page_size=500', { - headers: filter ? { 'x-filter': JSON.stringify(filter) } : {}, - }); - const data = await response.json(); - - if (data.data.pages > 1) { - throw new Error( - `Request ${endpoint} has many pages but we only support caching 1 page.` - ); - } - - writeFileSync(`${DATA_DIR}${filename}`, JSON.stringify(data)); - - console.log( - `Cached endpoint ${API_ROOT + endpoint} to ${DATA_DIR}${filename}` - ); -} - -async function prebuild() { - /** - * Request /types, /types-legacy and /regions and store the values - * as hard-coded JSON to save network requests on load - */ - console.log('Caching common requests'); - - const requests = cachedRequests.map((request) => - handleRequest(request.endpoint, request.filename, request.filter) - ); - - try { - await Promise.all(requests); - console.log('Caching successful'); - } catch (error) { - console.error('Caching failed', error); - } -} - -prebuild(); diff --git a/packages/manager/src/__data__/linodes.ts b/packages/manager/src/__data__/linodes.ts index e0c5d2a608a..cf97f0af27d 100644 --- a/packages/manager/src/__data__/linodes.ts +++ b/packages/manager/src/__data__/linodes.ts @@ -24,7 +24,12 @@ export const linode1: Linode = { ipv4: ['97.107.143.78', '98.107.143.78', '99.107.143.78'], ipv6: '2600:3c03::f03c:91ff:fe0a:109a/64', label: 'test', - placement_groups: [], + placement_group: { + affinity_type: 'anti_affinity', + id: 1, + is_strict: true, + label: 'pg-1', + }, region: 'us-east', specs: { disk: 20480, @@ -64,7 +69,12 @@ export const linode2: Linode = { ipv4: ['97.107.143.49'], ipv6: '2600:3c03::f03c:91ff:fe0a:0d7a/64', label: 'another-test', - placement_groups: [], + placement_group: { + affinity_type: 'anti_affinity', + id: 1, + is_strict: true, + label: 'pg-1', + }, region: 'us-east', specs: { disk: 30720, @@ -104,7 +114,12 @@ export const linode3: Linode = { ipv4: ['97.107.143.49'], ipv6: '2600:3c03::f03c:91ff:fe0a:0d7a/64', label: 'another-test', - placement_groups: [], + placement_group: { + affinity_type: 'anti_affinity', + id: 1, + is_strict: true, + label: 'pg-1', + }, region: 'us-east', specs: { disk: 30720, @@ -144,7 +159,12 @@ export const linode4: Linode = { ipv4: ['97.107.143.49'], ipv6: '2600:3c03::f03c:91ff:fe0a:0d7a/64', label: 'another-test-eu', - placement_groups: [], + placement_group: { + affinity_type: 'anti_affinity', + id: 1, + is_strict: true, + label: 'pg-1', + }, region: 'eu-west', specs: { disk: 30720, diff --git a/packages/manager/src/__data__/regionsData.ts b/packages/manager/src/__data__/regionsData.ts index c8afbb336d6..6e44c8dd739 100644 --- a/packages/manager/src/__data__/regionsData.ts +++ b/packages/manager/src/__data__/regionsData.ts @@ -24,6 +24,7 @@ export const regions: Region[] = [ ipv6: '2400:8904::f03c:91ff:fea5:659, 2400:8904::f03c:91ff:fea5:9282, 2400:8904::f03c:91ff:fea5:b9b3, 2400:8904::f03c:91ff:fea5:925a, 2400:8904::f03c:91ff:fea5:22cb, 2400:8904::f03c:91ff:fea5:227a, 2400:8904::f03c:91ff:fea5:924c, 2400:8904::f03c:91ff:fea5:f7e2, 2400:8904::f03c:91ff:fea5:2205, 2400:8904::f03c:91ff:fea5:9207', }, + site_type: 'core', status: 'ok', }, { @@ -48,6 +49,7 @@ export const regions: Region[] = [ ipv6: '2600:3c04::f03c:91ff:fea9:f63, 2600:3c04::f03c:91ff:fea9:f6d, 2600:3c04::f03c:91ff:fea9:f80, 2600:3c04::f03c:91ff:fea9:f0f, 2600:3c04::f03c:91ff:fea9:f99, 2600:3c04::f03c:91ff:fea9:fbd, 2600:3c04::f03c:91ff:fea9:fdd, 2600:3c04::f03c:91ff:fea9:fe2, 2600:3c04::f03c:91ff:fea9:f68, 2600:3c04::f03c:91ff:fea9:f4a', }, + site_type: 'core', status: 'ok', }, { @@ -72,6 +74,7 @@ export const regions: Region[] = [ ipv6: '2400:8907::f03c:92ff:fe6e:ec8, 2400:8907::f03c:92ff:fe6e:98e4, 2400:8907::f03c:92ff:fe6e:1c58, 2400:8907::f03c:92ff:fe6e:c299, 2400:8907::f03c:92ff:fe6e:c210, 2400:8907::f03c:92ff:fe6e:c219, 2400:8907::f03c:92ff:fe6e:1c5c, 2400:8907::f03c:92ff:fe6e:c24e, 2400:8907::f03c:92ff:fe6e:e6b, 2400:8907::f03c:92ff:fe6e:e3d', }, + site_type: 'core', status: 'ok', }, { @@ -98,6 +101,7 @@ export const regions: Region[] = [ ipv6: '2600:3c05::f03c:93ff:feb6:43b6, 2600:3c05::f03c:93ff:feb6:4365, 2600:3c05::f03c:93ff:feb6:43c2, 2600:3c05::f03c:93ff:feb6:e441, 2600:3c05::f03c:93ff:feb6:94ef, 2600:3c05::f03c:93ff:feb6:94ba, 2600:3c05::f03c:93ff:feb6:94a8, 2600:3c05::f03c:93ff:feb6:9413, 2600:3c05::f03c:93ff:feb6:9443, 2600:3c05::f03c:93ff:feb6:94e0', }, + site_type: 'core', status: 'ok', }, { @@ -124,6 +128,7 @@ export const regions: Region[] = [ ipv6: '2600:3c06::f03c:93ff:fed0:e5fc, 2600:3c06::f03c:93ff:fed0:e54b, 2600:3c06::f03c:93ff:fed0:e572, 2600:3c06::f03c:93ff:fed0:e530, 2600:3c06::f03c:93ff:fed0:e597, 2600:3c06::f03c:93ff:fed0:e511, 2600:3c06::f03c:93ff:fed0:e5f2, 2600:3c06::f03c:93ff:fed0:e5bf, 2600:3c06::f03c:93ff:fed0:e529, 2600:3c06::f03c:93ff:fed0:e5a3', }, + site_type: 'core', status: 'ok', }, { @@ -150,6 +155,7 @@ export const regions: Region[] = [ ipv6: '2600:3c07::f03c:93ff:fef2:2e63, 2600:3c07::f03c:93ff:fef2:2ec7, 2600:3c07::f03c:93ff:fef2:0dee, 2600:3c07::f03c:93ff:fef2:0d25, 2600:3c07::f03c:93ff:fef2:0de0, 2600:3c07::f03c:93ff:fef2:2e29, 2600:3c07::f03c:93ff:fef2:0dda, 2600:3c07::f03c:93ff:fef2:0d82, 2600:3c07::f03c:93ff:fef2:b3ac, 2600:3c07::f03c:93ff:fef2:b3a8', }, + site_type: 'core', status: 'ok', }, { @@ -175,6 +181,7 @@ export const regions: Region[] = [ ipv6: '2600:3c0a::f03c:93ff:fe54:c6da, 2600:3c0a::f03c:93ff:fe54:c691, 2600:3c0a::f03c:93ff:fe54:c68d, 2600:3c0a::f03c:93ff:fe54:c61e, 2600:3c0a::f03c:93ff:fe54:c653, 2600:3c0a::f03c:93ff:fe54:c64c, 2600:3c0a::f03c:93ff:fe54:c68a, 2600:3c0a::f03c:93ff:fe54:c697, 2600:3c0a::f03c:93ff:fe54:c60f, 2600:3c0a::f03c:93ff:fe54:c6a0', }, + site_type: 'core', status: 'ok', }, { @@ -200,6 +207,7 @@ export const regions: Region[] = [ ipv6: '2600:3c0d::f03c:93ff:fe3d:51cb, 2600:3c0d::f03c:93ff:fe3d:51a7, 2600:3c0d::f03c:93ff:fe3d:51a9, 2600:3c0d::f03c:93ff:fe3d:5119, 2600:3c0d::f03c:93ff:fe3d:51fe, 2600:3c0d::f03c:93ff:fe3d:517c, 2600:3c0d::f03c:93ff:fe3d:5144, 2600:3c0d::f03c:93ff:fe3d:5170, 2600:3c0d::f03c:93ff:fe3d:51cc, 2600:3c0d::f03c:93ff:fe3d:516c', }, + site_type: 'core', status: 'ok', }, { @@ -225,6 +233,7 @@ export const regions: Region[] = [ ipv6: '2600:3c0e::f03c:93ff:fe9d:2d10, 2600:3c0e::f03c:93ff:fe9d:2d89, 2600:3c0e::f03c:93ff:fe9d:2d79, 2600:3c0e::f03c:93ff:fe9d:2d96, 2600:3c0e::f03c:93ff:fe9d:2da5, 2600:3c0e::f03c:93ff:fe9d:2d34, 2600:3c0e::f03c:93ff:fe9d:2d68, 2600:3c0e::f03c:93ff:fe9d:2d17, 2600:3c0e::f03c:93ff:fe9d:2d45, 2600:3c0e::f03c:93ff:fe9d:2d5c', }, + site_type: 'core', status: 'ok', }, { @@ -250,6 +259,7 @@ export const regions: Region[] = [ ipv6: '2600:3c09::f03c:93ff:fea9:4dbe, 2600:3c09::f03c:93ff:fea9:4d63, 2600:3c09::f03c:93ff:fea9:4dce, 2600:3c09::f03c:93ff:fea9:4dbb, 2600:3c09::f03c:93ff:fea9:4d99, 2600:3c09::f03c:93ff:fea9:4d26, 2600:3c09::f03c:93ff:fea9:4de0, 2600:3c09::f03c:93ff:fea9:4d69, 2600:3c09::f03c:93ff:fea9:4dbf, 2600:3c09::f03c:93ff:fea9:4da6', }, + site_type: 'core', status: 'ok', }, { @@ -275,6 +285,7 @@ export const regions: Region[] = [ ipv6: '2600:3c08::f03c:93ff:fe7c:1135, 2600:3c08::f03c:93ff:fe7c:11f8, 2600:3c08::f03c:93ff:fe7c:11d2, 2600:3c08::f03c:93ff:fe7c:11a7, 2600:3c08::f03c:93ff:fe7c:11ad, 2600:3c08::f03c:93ff:fe7c:110a, 2600:3c08::f03c:93ff:fe7c:11f9, 2600:3c08::f03c:93ff:fe7c:1137, 2600:3c08::f03c:93ff:fe7c:11db, 2600:3c08::f03c:93ff:fe7c:1164', }, + site_type: 'core', status: 'ok', }, { @@ -300,6 +311,7 @@ export const regions: Region[] = [ ipv6: '2400:8905::f03c:93ff:fe9d:b085, 2400:8905::f03c:93ff:fe9d:b012, 2400:8905::f03c:93ff:fe9d:b09b, 2400:8905::f03c:93ff:fe9d:b0d8, 2400:8905::f03c:93ff:fe9d:259f, 2400:8905::f03c:93ff:fe9d:b006, 2400:8905::f03c:93ff:fe9d:b084, 2400:8905::f03c:93ff:fe9d:b0ce, 2400:8905::f03c:93ff:fe9d:25ea, 2400:8905::f03c:93ff:fe9d:b086', }, + site_type: 'core', status: 'ok', }, { @@ -325,6 +337,7 @@ export const regions: Region[] = [ ipv6: '2600:3c0b::f03c:93ff:feba:d513, 2600:3c0b::f03c:93ff:feba:d5c3, 2600:3c0b::f03c:93ff:feba:d597, 2600:3c0b::f03c:93ff:feba:d5fb, 2600:3c0b::f03c:93ff:feba:d51f, 2600:3c0b::f03c:93ff:feba:d58e, 2600:3c0b::f03c:93ff:feba:d5d5, 2600:3c0b::f03c:93ff:feba:d534, 2600:3c0b::f03c:93ff:feba:d57c, 2600:3c0b::f03c:93ff:feba:d529', }, + site_type: 'core', status: 'ok', }, { @@ -350,6 +363,7 @@ export const regions: Region[] = [ ipv6: '2a01:7e04::f03c:93ff:fead:d31f, 2a01:7e04::f03c:93ff:fead:d37f, 2a01:7e04::f03c:93ff:fead:d30c, 2a01:7e04::f03c:93ff:fead:d318, 2a01:7e04::f03c:93ff:fead:d316, 2a01:7e04::f03c:93ff:fead:d339, 2a01:7e04::f03c:93ff:fead:d367, 2a01:7e04::f03c:93ff:fead:d395, 2a01:7e04::f03c:93ff:fead:d3d0, 2a01:7e04::f03c:93ff:fead:d38e', }, + site_type: 'core', status: 'ok', }, { @@ -375,6 +389,7 @@ export const regions: Region[] = [ ipv6: '2600:3c0c::f03c:93ff:feed:a90b, 2600:3c0c::f03c:93ff:feed:a9a5, 2600:3c0c::f03c:93ff:feed:a935, 2600:3c0c::f03c:93ff:feed:a930, 2600:3c0c::f03c:93ff:feed:a95c, 2600:3c0c::f03c:93ff:feed:a9ad, 2600:3c0c::f03c:93ff:feed:a9f2, 2600:3c0c::f03c:93ff:feed:a9ff, 2600:3c0c::f03c:93ff:feed:a9c8, 2600:3c0c::f03c:93ff:feed:a96b', }, + site_type: 'core', status: 'ok', }, { @@ -400,6 +415,7 @@ export const regions: Region[] = [ ipv6: '2a01:7e03::f03c:93ff:feb1:b789, 2a01:7e03::f03c:93ff:feb1:b717, 2a01:7e03::f03c:93ff:feb1:b707, 2a01:7e03::f03c:93ff:feb1:b7ab, 2a01:7e03::f03c:93ff:feb1:b7e2, 2a01:7e03::f03c:93ff:feb1:b709, 2a01:7e03::f03c:93ff:feb1:b7a6, 2a01:7e03::f03c:93ff:feb1:b750, 2a01:7e03::f03c:93ff:feb1:b76e, 2a01:7e03::f03c:93ff:feb1:b7a2', }, + site_type: 'core', status: 'ok', }, { @@ -424,6 +440,7 @@ export const regions: Region[] = [ ipv6: '2600:3c00::2, 2600:3c00::9, 2600:3c00::7, 2600:3c00::5, 2600:3c00::3, 2600:3c00::8, 2600:3c00::6, 2600:3c00::4, 2600:3c00::c, 2600:3c00::b', }, + site_type: 'core', status: 'ok', }, { @@ -447,6 +464,7 @@ export const regions: Region[] = [ ipv6: '2600:3c01::2, 2600:3c01::9, 2600:3c01::5, 2600:3c01::7, 2600:3c01::3, 2600:3c01::8, 2600:3c01::4, 2600:3c01::b, 2600:3c01::c, 2600:3c01::6', }, + site_type: 'core', status: 'ok', }, { @@ -473,6 +491,7 @@ export const regions: Region[] = [ ipv6: '2600:3c02::3, 2600:3c02::5, 2600:3c02::4, 2600:3c02::6, 2600:3c02::c, 2600:3c02::7, 2600:3c02::2, 2600:3c02::9, 2600:3c02::8, 2600:3c02::b', }, + site_type: 'core', status: 'ok', }, { @@ -500,6 +519,7 @@ export const regions: Region[] = [ ipv6: '2600:3c03::7, 2600:3c03::4, 2600:3c03::9, 2600:3c03::6, 2600:3c03::3, 2600:3c03::c, 2600:3c03::5, 2600:3c03::b, 2600:3c03::2, 2600:3c03::8', }, + site_type: 'core', status: 'ok', }, { @@ -524,6 +544,7 @@ export const regions: Region[] = [ ipv6: '2a01:7e00::9, 2a01:7e00::3, 2a01:7e00::c, 2a01:7e00::5, 2a01:7e00::6, 2a01:7e00::8, 2a01:7e00::b, 2a01:7e00::4, 2a01:7e00::7, 2a01:7e00::2', }, + site_type: 'core', status: 'ok', }, { @@ -550,6 +571,7 @@ export const regions: Region[] = [ ipv6: '2400:8901::5, 2400:8901::4, 2400:8901::b, 2400:8901::3, 2400:8901::9, 2400:8901::2, 2400:8901::8, 2400:8901::7, 2400:8901::c, 2400:8901::6', }, + site_type: 'core', status: 'ok', }, { @@ -576,6 +598,23 @@ export const regions: Region[] = [ ipv6: '2a01:7e01::5, 2a01:7e01::9, 2a01:7e01::7, 2a01:7e01::c, 2a01:7e01::2, 2a01:7e01::4, 2a01:7e01::3, 2a01:7e01::6, 2a01:7e01::b, 2a01:7e01::8', }, + site_type: 'core', + status: 'ok', + }, + { + capabilities: ['Linodes'], + country: 'us', + id: 'us-edgetest', + label: 'Gecko Edge Test', + maximum_pgs_per_customer: 5, + maximum_vms_per_pg: 10, + resolvers: { + ipv4: + '139.162.130.5, 139.162.131.5, 139.162.132.5, 139.162.133.5, 139.162.134.5, 139.162.135.5, 139.162.136.5, 139.162.137.5, 139.162.138.5, 139.162.139.5', + ipv6: + '2a01:7e01::5, 2a01:7e01::9, 2a01:7e01::7, 2a01:7e01::c, 2a01:7e01::2, 2a01:7e01::4, 2a01:7e01::3, 2a01:7e01::6, 2a01:7e01::b, 2a01:7e01::8', + }, + site_type: 'edge', status: 'ok', }, ]; diff --git a/packages/manager/src/assets/icons/entityIcons/edge-server.svg b/packages/manager/src/assets/icons/entityIcons/edge-server.svg new file mode 100644 index 00000000000..1ca05241491 --- /dev/null +++ b/packages/manager/src/assets/icons/entityIcons/edge-server.svg @@ -0,0 +1,7 @@ + + +edge-server + + + + diff --git a/packages/manager/src/cachedData/kernels.json b/packages/manager/src/cachedData/kernels.json deleted file mode 100644 index 211aca02bdf..00000000000 --- a/packages/manager/src/cachedData/kernels.json +++ /dev/null @@ -1 +0,0 @@ -{"data":[{"id":"linode/latest-2.6-32bit","label":"Latest 2.6 (2.6.39.1-linode34)","version":"2.6.39","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2009-10-26T04:00:00"},{"id":"linode/latest-2.6","label":"Latest 2.6 Stable (2.6.23.17-linode44)","version":"2.6.24","kvm":false,"architecture":"i386","pvops":false,"deprecated":true,"built":"2009-08-17T04:00:00"},{"id":"linode/latest-32bit","label":"Latest 32 bit (6.2.9-x86-linode180)","version":"6.2.9","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2023-04-05T19:23:04"},{"id":"linode/2.6.18.8-linode22","label":"Latest Legacy (2.6.18.8-linode22)","version":"2.6.18","kvm":false,"architecture":"i386","pvops":false,"deprecated":true,"built":"2006-06-25T04:00:00"},{"id":"linode/6.4.9-x86_64-linode162","label":"6.4.9-x86_64-linode162","version":"6.4.9","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2023-08-09T19:47:08"},{"id":"linode/6.4.9-x86-linode182","label":"6.4.9-x86-linode182","version":"6.4.9","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2023-08-09T19:39:15"},{"id":"linode/6.3.5-x86_64-linode161","label":"6.3.5-x86_64-linode161","version":"6.3.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2023-05-31T16:28:44"},{"id":"linode/6.3.5-x86-linode181","label":"6.3.5-x86-linode181","version":"6.3.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2023-05-31T16:21:49"},{"id":"linode/6.2.9-x86_64-linode160","label":"6.2.9-x86_64-linode160","version":"6.2.9","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2023-04-05T19:30:32"},{"id":"linode/6.2.9-x86-linode180","label":"6.2.9-x86-linode180","version":"6.2.9","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2023-04-05T19:23:04"},{"id":"linode/6.1.10-x86_64-linode159","label":"6.1.10-x86_64-linode159","version":"6.1.10","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2023-02-08T19:14:45"},{"id":"linode/6.1.10-x86-linode179","label":"6.1.10-x86-linode179","version":"6.1.10","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2023-02-08T19:07:56"},{"id":"linode/6.0.10-x86_64-linode158","label":"6.0.10-x86_64-linode158","version":"6.0.10","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2022-12-01T18:16:43"},{"id":"linode/6.0.10-x86-linode178","label":"6.0.10-x86-linode178","version":"6.0.10","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2022-12-01T18:09:33"},{"id":"linode/6.0.2-x86_64-linode157","label":"6.0.2-x86_64-linode157","version":"6.0.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2022-10-17T17:01:41"},{"id":"linode/6.0.2-x86-linode177","label":"6.0.2-x86-linode177","version":"6.0.2","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2022-10-17T16:54:28"},{"id":"linode/5.19.2-x86_64-linode156","label":"5.19.2-x86_64-linode156","version":"5.19.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2022-08-18T19:51:13"},{"id":"linode/5.19.2-x86-linode176","label":"5.19.2-x86-linode176","version":"5.19.2","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2022-08-18T19:44:26"},{"id":"linode/5.18.2-x86_64-linode155","label":"5.18.2-x86_64-linode155","version":"5.18.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2022-06-07T14:46:11"},{"id":"linode/5.18.2-x86-linode175","label":"5.18.2-x86-linode175","version":"5.18.2","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2022-06-07T14:39:32"},{"id":"linode/5.17.5-x86_64-linode154","label":"5.17.5-x86_64-linode154","version":"5.17.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2022-05-02T19:07:22"},{"id":"linode/5.17.5-x86-linode174","label":"5.17.5-x86-linode174","version":"5.17.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2022-05-02T19:00:48"},{"id":"linode/5.16.13-x86-linode173","label":"5.16.13-x86-linode173","version":"5.16.13","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2022-03-08T19:09:29"},{"id":"linode/5.16.13-x86_64-linode153","label":"5.16.13-x86_64-linode153","version":"5.16.13","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2022-03-08T19:16:05"},{"id":"linode/5.16.3-x86_64-linode152","label":"5.16.3-x86_64-linode152","version":"5.16.3","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2022-01-27T19:46:44"},{"id":"linode/5.16.3-x86-linode172","label":"5.16.3-x86-linode172","version":"5.16.3","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2022-01-27T19:40:10"},{"id":"linode/5.15.10-x86_64-linode151","label":"5.15.10-x86_64-linode151","version":"5.15.10","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2021-12-21T18:44:00"},{"id":"linode/5.15.10-x86-linode171","label":"5.15.10-x86-linode171","version":"5.15.10","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2021-12-21T18:37:00"},{"id":"linode/5.14.17-x86_64-linode150","label":"5.14.17-x86_64-linode150","version":"5.14.17","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-11-11T18:23:00"},{"id":"linode/5.14.17-x86-linode170","label":"5.14.17-x86-linode170","version":"5.14.17","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-11-11T18:17:00"},{"id":"linode/5.14.15-x86_64-linode149","label":"5.14.15-x86_64-linode149","version":"5.14.15","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-11-01T18:02:00"},{"id":"linode/5.14.15-x86-linode169","label":"5.14.15-x86-linode169","version":"5.14.15","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-11-01T17:55:00"},{"id":"linode/5.14.14-x86_64-linode148","label":"5.14.14-x86_64-linode148","version":"5.14.14","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-10-20T18:22:00"},{"id":"linode/5.14.14-x86-linode168","label":"5.14.14-x86-linode168","version":"5.14.14","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-10-20T18:15:00"},{"id":"linode/5.14.2-x86_64-linode147","label":"5.14.2-x86_64-linode147","version":"5.14.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-09-08T19:06:00"},{"id":"linode/5.14.2-x86-linode167","label":"5.14.2-x86-linode167","version":"5.14.2","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-09-08T19:00:00"},{"id":"linode/5.13.4-x86_64-linode146","label":"5.13.4-x86_64-linode146","version":"5.13.4","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-07-21T18:51:00"},{"id":"linode/5.13.4-x86-linode166","label":"5.13.4-x86-linode166","version":"5.13.4","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-07-21T18:45:00"},{"id":"linode/5.12.13-x86_64-linode145","label":"5.12.13-x86_64-linode145","version":"5.12.13","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-06-24T20:24:00"},{"id":"linode/5.12.13-x86-linode165","label":"5.12.13-x86-linode165","version":"5.12.13","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-06-24T20:19:00"},{"id":"linode/5.12.2-x86_64-linode144","label":"5.12.2-x86_64-linode144","version":"5.12.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-05-10T17:16:00"},{"id":"linode/5.12.2-x86-linode164","label":"5.12.2-x86-linode164","version":"5.12.2","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-05-10T17:10:00"},{"id":"linode/5.11.13-x86_64-linode143","label":"5.11.13-x86_64-linode143","version":"5.11.13","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-04-13T15:46:00"},{"id":"linode/5.11.13-x86-linode163","label":"5.11.13-x86-linode163","version":"5.11.13","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-04-13T15:40:00"},{"id":"linode/5.11.9-x86_64-linode142","label":"5.11.9-x86_64-linode142","version":"5.11.9","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2021-03-24T15:33:00"},{"id":"linode/5.11.9-x86-linode162","label":"5.11.9-x86-linode162","version":"5.11.9","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2021-03-24T15:28:00"},{"id":"linode/5.10.13-x86_64-linode141","label":"5.10.13-x86_64-linode141","version":"5.10.13","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2021-02-04T19:02:00"},{"id":"linode/5.10.13-x86-linode161","label":"5.10.13-x86-linode161","version":"5.10.13","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2021-02-04T18:56:00"},{"id":"linode/5.10.2-x86_64-linode140","label":"5.10.2-x86_64-linode140","version":"5.10.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2020-12-22T20:43:00"},{"id":"linode/5.10.2-x86-linode160","label":"5.10.2-x86-linode160","version":"5.10.2","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2020-12-22T20:38:00"},{"id":"linode/5.9.6-x86_64-linode139","label":"5.9.6-x86_64-linode139","version":"5.9.6","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2020-11-05T19:51:00"},{"id":"linode/5.9.6-x86-linode159","label":"5.9.6-x86-linode159","version":"5.9.6","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2020-11-05T19:45:00"},{"id":"linode/5.8.10-x86-linode158","label":"5.8.10-x86-linode158","version":"5.8.10","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2020-09-17T19:58:00"},{"id":"linode/5.8.10-x86_64-linode138","label":"5.8.10-x86_64-linode138","version":"5.8.10","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2020-09-17T20:03:00"},{"id":"linode/5.8.3-x86_64-linode137","label":"5.8.3-x86_64-linode137","version":"5.8.3","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2020-08-24T18:55:00"},{"id":"linode/5.8.3-x86-linode157","label":"5.8.3-x86-linode157","version":"5.8.3","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2020-08-24T18:50:00"},{"id":"linode/5.7.6-x86-linode156","label":"5.7.6-x86-linode156","version":"5.7.6","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2020-06-25T16:41:08"},{"id":"linode/5.6.14-x86-linode155","label":"5.6.14-x86-linode155","version":"5.6.14","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2020-06-01T14:05:47"},{"id":"linode/5.6.1-x86-linode154","label":"5.6.1-x86-linode154","version":"5.6.1","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2020-04-07T21:29:12"},{"id":"linode/5.4.10-x86-linode152","label":"5.4.10-x86-linode152","version":"5.4.10","kvm":true,"architecture":"i386","pvops":true,"deprecated":false,"built":"2020-01-10T21:02:10"},{"id":"linode/5.3.11-x86-linode151","label":"5.3.11-x86-linode151","version":"5.3.11","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-11-14T20:38:53"},{"id":"linode/5.3.7-x86-linode150","label":"5.3.7-x86-linode150","version":"5.3.7","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-10-30T21:10:08"},{"id":"linode/5.2.9-x86-linode149","label":"5.2.9-x86-linode149","version":"5.2.9","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-08-21T18:48:52"},{"id":"linode/5.1.17-x86-linode148","label":"5.1.17-x86-linode148","version":"5.1.17","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-07-16T15:25:35"},{"id":"linode/5.1.11-x86-linode147","label":"5.1.11-x86-linode147","version":"5.1.11","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-06-17T22:46:37"},{"id":"linode/5.1.5-x86-linode146","label":"5.1.5-x86-linode146","version":"5.1.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-05-30T20:30:37"},{"id":"linode/4.14.120-x86-linode145","label":"4.14.120-x86-linode145","version":"4.14.120","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-05-21T10:57:13"},{"id":"linode/5.1.2-x86-linode144","label":"5.1.2-x86-linode144","version":"5.1.2","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-05-15T16:13:49"},{"id":"linode/5.0.8-x86-linode143","label":"5.0.8-x86-linode143","version":"5.0.8","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-04-17T18:39:29"},{"id":"linode/4.20.4-x86-linode141","label":"4.20.4-x86-linode141","version":"4.20.4","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2019-01-25T19:13:27"},{"id":"linode/4.19.8-x86-linode140","label":"4.19.8-x86-linode140","version":"4.19.8","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-12-12T21:44:08"},{"id":"linode/4.19.5-x86-linode139","label":"4.19.5-x86-linode139","version":"4.19.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-11-29T16:36:09"},{"id":"linode/4.18.16-x86-linode138","label":"4.18.16-x86-linode138","version":"4.18.16","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-10-29T20:24:50"},{"id":"linode/4.18.8-x86-linode137","label":"4.18.8-x86-linode137","version":"4.18.8","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-10-04T18:06:19"},{"id":"linode/4.18.8-x86-linode136","label":"4.18.8-x86-linode136","version":"4.18.8","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-09-19T12:54:57"},{"id":"linode/4.17.17-x86-linode135","label":"4.17.17-x86-linode135","version":"4.17.17","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-08-20T17:24:09"},{"id":"linode/4.17.15-x86-linode134","label":"4.17.15-x86-linode134","version":"4.17.15","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-08-16T17:13:56"},{"id":"linode/4.17.14-x86-linode133","label":"4.17.14-x86-linode133","version":"4.17.14","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-08-13T16:40:31"},{"id":"linode/4.17.14-x86-linode132","label":"4.17.14-x86-linode132","version":"4.17.14","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-08-10T21:08:51"},{"id":"linode/4.17.12-x86-linode131","label":"4.17.12-x86-linode131","version":"4.17.12","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-08-07T13:01:28"},{"id":"linode/4.17.11-x86-linode130","label":"4.17.11-x86-linode130","version":"4.17.11","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-07-31T19:51:00"},{"id":"linode/4.17.8-x86-linode129","label":"4.17.8-x86-linode129","version":"4.17.8","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-07-18T17:17:29"},{"id":"linode/4.17.2-x86-linode128","label":"4.17.2-x86-linode128","version":"4.17.2","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-06-26T19:42:55"},{"id":"linode/4.16.11-x86-linode127","label":"4.16.11-x86-linode127","version":"4.16.11","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-05-24T21:33:29"},{"id":"linode/4.15.18-x86-linode126","label":"4.15.18-x86-linode126","version":"4.15.18","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-05-02T20:21:02"},{"id":"linode/4.15.13-x86-linode125","label":"4.15.13-x86-linode125","version":"4.15.13","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-03-27T17:17:56"},{"id":"linode/4.15.12-x86-linode124","label":"4.15.12-x86-linode124","version":"4.15.12","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-03-22T20:09:16"},{"id":"linode/4.15.10-x86-linode123","label":"4.15.10-x86-linode123","version":"4.15.10","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-03-16T21:18:40"},{"id":"linode/4.15.8-x86-linode122","label":"4.15.8-x86-linode122","version":"4.15.8","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-03-10T21:25:42"},{"id":"linode/4.15.7-x86-linode121","label":"4.15.7-x86-linode121","version":"4.15.7","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-03-01T17:20:09"},{"id":"linode/4.14.19-x86-linode119","label":"4.14.19-x86-linode119","version":"4.14.19","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-02-13T19:05:44"},{"id":"linode/4.14.17-x86-linode118","label":"4.14.17-x86-linode118","version":"4.14.17","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-02-08T18:34:40"},{"id":"linode/4.9.80-x86-linode117","label":"4.9.80-x86-linode117","version":"4.9.80","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-02-08T18:33:46"},{"id":"linode/4.4.115-x86-linode116","label":"4.4.115-x86-linode116","version":"4.4.115","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-02-08T18:32:57"},{"id":"linode/4.4.113-x86-linode115","label":"4.4.113-x86-linode115","version":"4.4.113","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-01-25T22:47:11"},{"id":"linode/4.9.78-x86-linode114","label":"4.9.78-x86-linode114","version":"4.9.78","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-01-23T23:23:58"},{"id":"linode/4.14.14-x86-linode113","label":"4.14.14-x86-linode113","version":"4.14.14","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-01-23T02:00:02"},{"id":"linode/4.14.14-x86-linode112","label":"4.14.14-x86-linode112","version":"4.14.14","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-01-18T20:09:53"},{"id":"linode/4.9.64-x86-linode107","label":"4.9.64-x86-linode107","version":"4.9.64","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2017-12-01T23:10:11"},{"id":"linode/4.9.68-x86-linode108","label":"4.9.68-x86-linode108","version":"4.9.68","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2017-12-11T19:00:15"},{"id":"linode/4.14.12-x86-linode111","label":"4.14.12-x86-linode111","version":"4.14.12","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-01-05T16:31:55"},{"id":"linode/4.14.11-x86-linode110","label":"4.14.11-x86-linode110","version":"4.14.11","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2018-01-04T18:56:25"},{"id":"linode/4.9.56-x86-linode106","label":"4.9.56-x86-linode106","version":"4.9.56","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2017-10-13T21:10:23"},{"id":"linode/4.9.50-x86-linode105","label":"4.9.50-x86-linode105","version":"4.9.50","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2017-09-14T21:46:56"},{"id":"linode/4.9.36-x86-linode104","label":"4.9.36-x86-linode104","version":"4.9.36","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2017-07-10T13:16:53"},{"id":"linode/4.9.33-x86-linode102","label":"4.9.33-x86-linode102","version":"4.9.33","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2017-06-23T22:06:05"},{"id":"linode/4.9.15-x86-linode100","label":"4.9.15-x86-linode100","version":"4.9.15","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2017-03-22T13:48:13"},{"id":"linode/4.9.7-x86-linode99","label":"4.9.7-x86-linode99","version":"4.9.7","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2017-02-03T22:54:57"},{"id":"linode/4.9.0-x86-linode98","label":"4.9.0-x86-linode98","version":"4.9.0","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-12-13T20:10:20"},{"id":"linode/4.8.6-x86-linode97","label":"4.8.6-x86-linode97","version":"4.8.6","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-11-02T15:23:43"},{"id":"linode/4.8.4-x86-linode96","label":"4.8.4-x86-linode96","version":"4.8.4","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-10-27T18:51:41"},{"id":"linode/4.8.3-x86-linode95","label":"4.8.3-x86-linode95","version":"4.8.3","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-10-20T23:10:27"},{"id":"linode/4.8.1-x86-linode94","label":"4.8.1-x86-linode94","version":"4.8.1","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-10-07T22:21:55"},{"id":"linode/4.7.3-x86-linode92","label":"4.7.3-x86-linode92","version":"4.7.3","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-09-15T13:13:40"},{"id":"linode/4.7.0-x86-linode90","label":"4.7.0-x86-linode90","version":"4.7.0","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-08-05T14:35:48"},{"id":"linode/4.6.5-x86-linode89","label":"4.6.5-x86-linode89","version":"4.6.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-08-04T15:28:59"},{"id":"linode/4.5.5-x86-linode88","label":"4.5.5-x86-linode88","version":"4.5.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-05-24T15:29:02"},{"id":"linode/4.5.3-x86-linode86","label":"4.5.3-x86-linode86","version":"4.5.3","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-05-10T19:39:51"},{"id":"linode/4.5.0-x86-linode84","label":"4.5.0-x86-linode84","version":"4.5.0","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-03-16T18:53:02"},{"id":"linode/4.4.4-x86-linode83","label":"4.4.4-x86-linode83","version":"4.4.4","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-03-10T22:20:19"},{"id":"linode/4.4.0-x86-linode82","label":"4.4.0-x86-linode82","version":"4.4.0","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2016-01-20T14:41:05"},{"id":"linode/4.1.5-x86-linode80","label":"4.1.5-x86-linode80","version":"4.1.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-08-24T19:00:43"},{"id":"linode/4.1.5-x86-linode79","label":"4.1.5-x86-linode79","version":"4.1.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-08-13T13:00:00"},{"id":"linode/4.1.0-x86-linode78","label":"4.1.0-x86-linode78","version":"4.1.0","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-06-22T15:19:32"},{"id":"linode/4.0.5-x86-linode77","label":"4.0.5-x86-linode77","version":"4.0.5","kvm":true,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-06-11T13:58:18"},{"id":"linode/4.0.5-x86-linode76","label":"4.0.5-x86-linode76","version":"4.0.5","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-06-10T15:31:52"},{"id":"linode/4.0.4-x86-linode75","label":"4.0.4-x86-linode75","version":"4.0.4","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-05-21T15:15:47"},{"id":"linode/4.0.2-x86-linode74","label":"4.0.2-x86-linode74","version":"4.0.2","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-05-11T20:56:58"},{"id":"linode/4.0-x86-linode73","label":"4.0.1-x86-linode73","version":"4.0.1","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-05-04T13:43:23"},{"id":"linode/4.0-x86-linode72","label":"4.0-x86-linode72","version":"4.0","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-04-21T13:02:24"},{"id":"linode/3.19.1-x86-linode71","label":"3.19.1-x86-linode71","version":"3.19.1","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-03-11T18:00:36"},{"id":"linode/3.18.5-x86-linode70","label":"3.18.5-x86-linode70","version":"3.18.5","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-02-05T05:00:00"},{"id":"linode/3.18.3-x86-linode69","label":"3.18.3-x86-linode69","version":"3.18.3","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-01-23T15:12:45"},{"id":"linode/3.18.1-x86-linode68","label":"3.18.1-x86-linode68","version":"3.18.1","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2015-01-06T17:32:39"},{"id":"linode/3.16.7-x86-linode67","label":"3.16.7-x86-linode67","version":"3.16.7","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-11-14T22:31:46"},{"id":"linode/3.16.5-x86-linode65","label":"3.16.5-x86-linode65","version":"3.16.5","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-10-13T13:40:00"},{"id":"linode/3.15.4-x86-linode64","label":"3.15.4-x86-linode64","version":"3.15.4","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-07-07T13:50:35"},{"id":"linode/3.15.3-x86-linode63","label":"3.15.3-x86-linode63","version":"3.15.3","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-07-02T12:12:37"},{"id":"linode/3.15.2-x86-linode62","label":"3.15.2-x86-linode62","version":"3.15.2","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-06-30T18:46:50"},{"id":"linode/3.14.5-x86-linode61","label":"3.14.5-x86-linode61","version":"3.14.5","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-06-05T20:05:44"},{"id":"linode/3.14.5-x86-linode60","label":"3.14.5-x86-linode60","version":"3.14.5","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-06-03T13:09:58"},{"id":"linode/3.14.4-x86-linode59","label":"3.14.4-x86-linode59","version":"3.14.4","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-05-13T17:42:22"},{"id":"linode/3.14.1-x86-linode58","label":"3.14.1-x86-linode58","version":"3.14.1","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-04-25T17:49:15"},{"id":"linode/3.13.7-x86-linode57","label":"3.13.7-x86-linode57","version":"3.13.7","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-03-25T18:21:50"},{"id":"linode/3.12.9-x86-linode56","label":"3.12.9-x86-linode56","version":"3.12.9","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2014-02-03T19:42:13"},{"id":"linode/3.11.6-x86-linode54","label":"3.11.6-x86-linode54","version":"3.11.6","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2013-10-23T16:06:29"},{"id":"linode/3.12.6-x86-linode55","label":"3.12.6-x86-linode55","version":"3.12.6","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2013-12-23T16:25:39"},{"id":"linode/3.10.3-x86-linode53","label":"3.10.3-x86-linode53","version":"3.10.3","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2013-07-26T16:35:12"},{"id":"linode/3.9.3-x86-linode52","label":"3.9.3-x86-linode52","version":"3.9.3","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2013-05-20T14:27:27"},{"id":"linode/3.9.2-x86-linode51","label":"3.9.2-x86-linode51","version":"3.9.2","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2013-05-14T16:13:27"},{"id":"linode/3.8.4-linode50","label":"3.8.4-linode50","version":"3.8.4","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2013-03-25T20:42:49"},{"id":"linode/3.7.10-linode49","label":"3.7.10-linode49","version":"3.7.10","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2013-02-27T19:49:45"},{"id":"linode/3.7.5-linode48","label":"3.7.5-linode48","version":"3.7.5","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2013-01-31T19:52:25"},{"id":"linode/3.6.5-linode47","label":"3.6.5-linode47","version":"3.6.5","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2012-11-04T17:42:14"},{"id":"linode/3.5.3-linode46","label":"3.5.3-linode46","version":"3.5.3","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2012-09-05T20:45:36"},{"id":"linode/3.5.2-linode45","label":"3.5.2-linode45","version":"3.5.2","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2012-08-15T18:16:29"},{"id":"linode/3.4.2-linode44","label":"3.4.2-linode44","version":"3.4.2","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2012-06-11T19:03:10"},{"id":"linode/3.0.18-linode43","label":"3.0.18-linode43","version":"3.0.18","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2012-01-30T17:42:21"},{"id":"linode/3.1.10-linode42","label":"3.1.10-linode42","version":"3.1.10","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2012-01-25T21:24:07"},{"id":"linode/3.0.17-linode41","label":"3.0.17-linode41","version":"3.0.17","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2012-01-25T21:24:05"},{"id":"linode/3.2.1-linode40","label":"3.2.1-linode40","version":"3.2.0","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2012-01-23T16:04:48"},{"id":"linode/3.1.0-linode39","label":"3.1.0-linode39","version":"3.1.0","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-10-25T17:57:05"},{"id":"linode/3.0.4-linode38","label":"3.0.4-linode38","version":"3.0.4","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-09-22T18:57:59"},{"id":"linode/3.0.4-linode37","label":"3.0.4-linode37","version":"3.0.4","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-09-12T16:03:31"},{"id":"linode/3.0.4-linode36","label":"3.0.4-linode36","version":"3.0.4","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-09-02T01:08:55"},{"id":"linode/3.0-linode35","label":"3.0.0-linode35","version":"3.0.0","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-08-02T15:43:52"},{"id":"linode/2.6.39.1-linode34","label":"2.6.39.1-linode34","version":"2.6.39","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-06-21T14:42:50"},{"id":"linode/2.6.39-linode33","label":"2.6.39-linode33","version":"2.6.39","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-05-25T19:05:05"},{"id":"linode/2.6.38.3-linode32","label":"2.6.38.3-linode32","version":"2.6.38","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-04-21T20:21:48"},{"id":"linode/2.6.38-linode31","label":"2.6.38-linode31","version":"2.6.38","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-03-21T21:44:09"},{"id":"linode/2.6.37-linode30","label":"2.6.37-linode30","version":"2.6.37","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2011-01-27T05:00:00"},{"id":"linode/2.6.35.7-linode29","label":"2.6.35.7-linode29","version":"2.6.35","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2010-10-13T04:00:00"},{"id":"linode/2.6.32.16-linode28","label":"2.6.32.16-linode28","version":"2.6.32","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2010-07-25T21:34:00"},{"id":"linode/2.6.34-linode27","label":"2.6.34-linode27","version":"2.6.34","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2010-07-16T04:00:00"},{"id":"linode/2.6.32.12-linode25","label":"2.6.32.12-linode25","version":"2.6.33","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2010-04-28T04:00:00"},{"id":"linode/2.6.33-linode24","label":"2.6.33-linode24","version":"2.6.33","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2010-02-24T22:05:00"},{"id":"linode/2.6.32-linode23","label":"2.6.32-linode23","version":"2.6.32","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2009-12-05T16:14:00"},{"id":"linode/2.6.18.8-linode22","label":"2.6.18.8-linode22","version":"2.6.18","kvm":false,"architecture":"i386","pvops":false,"deprecated":true,"built":"2009-11-10T05:00:00"},{"id":"linode/2.6.31.5-linode21","label":"2.6.31.5-linode21","version":"2.6.31","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2009-10-26T04:00:00"},{"id":"linode/2.6.30.5-linode20","label":"2.6.30.5-linode20","version":"2.6.30","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2009-08-17T04:00:00"},{"id":"linode/2.6.23.17-linode44","label":"2.6.23.17-linode44","version":"2.6.23","kvm":false,"architecture":"i386","pvops":false,"deprecated":true,"built":"2009-08-17T04:00:00"},{"id":"linode/2.6.18.8-linode19","label":"2.6.18.8-linode19","version":"2.6.18","kvm":false,"architecture":"i386","pvops":false,"deprecated":true,"built":"2009-08-14T04:00:00"},{"id":"linode/2.6.29-linode18","label":"2.6.29-linode18","version":"2.6.29","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2009-04-01T04:00:00"},{"id":"linode/2.6.28.3-linode17","label":"2.6.28.3-linode17","version":"2.6.28","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2009-02-04T05:00:00"},{"id":"linode/2.6.18.8-linode16","label":"2.6.18.8-linode16","version":"2.6.18","kvm":false,"architecture":"i386","pvops":false,"deprecated":true,"built":"2009-01-12T14:47:00"},{"id":"linode/2.6.28-linode15","label":"2.6.28-linode15","version":"2.6.28","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2008-12-29T05:00:00"},{"id":"linode/2.6.27.4-linode14","label":"2.6.27.4-linode14","version":"2.6.27","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2008-11-05T05:00:00"},{"id":"linode/2.6.26-linode13","label":"2.6.26-linode13","version":"2.6.26","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2008-07-13T23:15:00"},{"id":"linode/2.6.25.10-linode12","label":"2.6.25.10-linode12","version":"2.6.25","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2008-07-03T04:00:00"},{"id":"linode/2.6.18.8-linode10","label":"2.6.18.8-linode10","version":"2.6.18","kvm":false,"architecture":"i386","pvops":false,"deprecated":true,"built":"2008-06-23T04:00:00"},{"id":"linode/2.6.25-linode9","label":"2.6.25-linode9","version":"2.6.25","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2008-04-10T04:00:00"},{"id":"linode/2.6.24.4-linode8","label":"2.6.24.4-linode8","version":"2.6.24","kvm":false,"architecture":"i386","pvops":true,"deprecated":true,"built":"2008-03-31T04:00:00"},{"id":"linode/2.6.18.8-domU-linode7","label":"2.6.18.8-domU-linode7","version":"2.6.18","kvm":false,"architecture":"i386","pvops":false,"deprecated":true,"built":null},{"id":"linode/latest-2.6-64bit","label":"Latest 2.6 (2.6.39.1-x86_64-linode19)","version":"2.6.39","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2009-10-26T04:00:00"},{"id":"linode/latest-64bit","label":"Latest 64 bit (6.2.9-x86_64-linode160)","version":"6.2.9","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2023-04-05T19:30:32"},{"id":"linode/2.6.18.8-x86_64-linode10","label":"Latest Legacy (2.6.18.8-x86_64-linode10)","version":"2.6.18","kvm":false,"architecture":"x86_64","pvops":false,"deprecated":true,"built":"2009-08-17T04:00:00"},{"id":"linode/5.7.6-x86_64-linode136","label":"5.7.6-x86_64-linode136","version":"5.7.6","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2020-06-25T16:41:35"},{"id":"linode/5.6.14-x86_64-linode135","label":"5.6.14-x86_64-linode135","version":"5.6.14","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2020-06-01T14:06:45"},{"id":"linode/5.6.1-x86_64-linode134","label":"5.6.1-x86_64-linode134","version":"5.6.1","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2020-04-07T21:29:51"},{"id":"linode/5.4.10-x86_64-linode132","label":"5.4.10-x86_64-linode132","version":"5.4.10","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":false,"built":"2020-01-10T21:03:16"},{"id":"linode/5.3.11-x86_64-linode131","label":"5.3.11-x86_64-linode131","version":"5.3.11","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-11-14T20:39:27"},{"id":"linode/5.3.7-x86_64-linode130","label":"5.3.7-x86_64-linode130","version":"5.3.7","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-10-30T21:10:29"},{"id":"linode/5.2.9-x86_64-linode129","label":"5.2.9-x86_64-linode129","version":"5.2.9","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-08-21T18:49:31"},{"id":"linode/5.1.17-x86_64-linode128","label":"5.1.17-x86_64-linode128","version":"5.1.17","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-07-16T15:26:33"},{"id":"linode/5.1.11-x86_64-linode127","label":"5.1.11-x86_64-linode127","version":"5.1.11","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-06-17T22:47:20"},{"id":"linode/5.1.5-x86_64-linode126","label":"5.1.5-x86_64-linode126","version":"5.1.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-05-30T20:30:39"},{"id":"linode/4.14.120-x86_64-linode125","label":"4.14.120-x86_64-linode125","version":"4.14.120","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-05-21T10:57:46"},{"id":"linode/5.1.2-x86_64-linode124","label":"5.1.2-x86_64-linode124","version":"5.1.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-05-15T16:14:35"},{"id":"linode/5.0.8-x86_64-linode123","label":"5.0.8-x86_64-linode123","version":"5.0.8","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-04-17T18:39:56"},{"id":"linode/5.0.1-x86_64-linode122","label":"5.0.1-x86_64-linode122","version":"5.0.1","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-03-13T16:51:01"},{"id":"linode/4.20.4-x86_64-linode121","label":"4.20.4-x86_64-linode121","version":"4.20.4","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2019-01-25T19:13:29"},{"id":"linode/4.19.8-x86_64-linode120","label":"4.19.8-x86_64-linode120","version":"4.19.8","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-12-12T21:44:08"},{"id":"linode/4.19.5-x86_64-linode119","label":"4.19.5-x86_64-linode119","version":"4.19.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-11-29T16:36:53"},{"id":"linode/4.18.16-x86_64-linode118","label":"4.18.16-x86_64-linode118","version":"4.18.16","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-10-29T20:24:13"},{"id":"linode/4.18.8-x86_64-linode117","label":"4.18.8-x86_64-linode117","version":"4.18.8","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-09-19T12:55:56"},{"id":"linode/4.17.17-x86_64-linode116","label":"4.17.17-x86_64-linode116","version":"4.17.17","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-08-20T17:23:32"},{"id":"linode/4.17.15-x86_64-linode115","label":"4.17.15-x86_64-linode115","version":"4.17.15","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-08-16T17:13:28"},{"id":"linode/4.17.14-x86_64-linode114","label":"4.17.14-x86_64-linode114","version":"4.17.14","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-08-13T16:41:06"},{"id":"linode/4.17.14-x86_64-linode113","label":"4.17.14-x86_64-linode113","version":"4.17.14","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-08-10T21:07:56"},{"id":"linode/4.17.12-x86_64-linode112","label":"4.17.12-x86_64-linode112","version":"4.17.12","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-08-07T13:02:24"},{"id":"linode/4.17.11-x86_64-linode111","label":"4.17.11-x86_64-linode111","version":"4.17.11","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-07-31T19:51:53"},{"id":"linode/4.17.8-x86_64-linode110","label":"4.17.8-x86_64-linode110","version":"4.17.8","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-07-18T17:18:30"},{"id":"linode/4.17.2-x86_64-linode109","label":"4.17.2-x86_64-linode109","version":"4.17.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-06-26T19:42:57"},{"id":"linode/4.16.11-x86_64-linode108","label":"4.16.11-x86_64-linode108","version":"4.16.11","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-05-24T21:33:31"},{"id":"linode/4.15.18-x86_64-linode107","label":"4.15.18-x86_64-linode107","version":"4.15.18","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-05-02T20:21:04"},{"id":"linode/4.15.13-x86_64-linode106","label":"4.15.13-x86_64-linode106","version":"4.15.13","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-03-27T17:17:56"},{"id":"linode/4.15.12-x86_64-linode105","label":"4.15.12-x86_64-linode105","version":"4.15.12","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-03-22T20:08:43"},{"id":"linode/4.15.10-x86_64-linode104","label":"4.15.10-x86_64-linode104","version":"4.15.10","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-03-16T21:18:35"},{"id":"linode/4.15.8-x86_64-linode103","label":"4.15.8-x86_64-linode103","version":"4.15.8","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-03-10T21:25:43"},{"id":"linode/4.15.7-x86_64-linode102","label":"4.15.7-x86_64-linode102","version":"4.15.7","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-03-01T17:20:54"},{"id":"linode/4.14.19-x86_64-linode100","label":"4.14.19-x86_64-linode100","version":"4.14.19","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-02-13T19:07:46"},{"id":"linode/4.14.17-x86_64-linode99","label":"4.14.17-x86_64-linode99","version":"4.14.17","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-02-08T18:35:09"},{"id":"linode/4.9.80-x86_64-linode98","label":"4.9.80-x86_64-linode98","version":"4.9.80","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-02-08T18:34:16"},{"id":"linode/4.4.115-x86_64-linode97","label":"4.4.115-x86_64-linode97","version":"4.4.115","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-02-08T18:33:23"},{"id":"linode/4.4.113-x86_64-linode96","label":"4.4.113-x86_64-linode96","version":"4.4.113","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-01-25T22:47:11"},{"id":"linode/4.9.78-x86_64-linode95","label":"4.9.78-x86_64-linode95","version":"4.9.78","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-01-23T23:23:58"},{"id":"linode/4.14.14-x86_64-linode94","label":"4.14.14-x86_64-linode94","version":"4.14.14","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-01-23T02:10:08"},{"id":"linode/4.14.14-x86_64-linode93","label":"4.14.14-x86_64-linode93","version":"4.14.14","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-01-18T20:08:56"},{"id":"linode/4.9.64-x86_64-linode88","label":"4.9.64-x86_64-linode88","version":"4.9.64","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2017-12-01T23:10:11"},{"id":"linode/4.9.68-x86_64-linode89","label":"4.9.68-x86_64-linode89","version":"4.9.68","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2017-12-11T19:00:48"},{"id":"linode/4.14.12-x86_64-linode92","label":"4.14.12-x86_64-linode92","version":"4.14.12","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-01-05T16:31:28"},{"id":"linode/4.14.11-x86_64-linode91","label":"4.14.11-x86_64-linode91","version":"4.14.11","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2018-01-04T18:56:22"},{"id":"linode/4.9.56-x86_64-linode87","label":"4.9.56-x86_64-linode87","version":"4.9.56","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2017-10-13T21:09:35"},{"id":"linode/4.9.50-x86_64-linode86","label":"4.9.50-x86_64-linode86","version":"4.9.50","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2017-09-14T21:46:25"},{"id":"linode/4.9.36-x86_64-linode85","label":"4.9.36-x86_64-linode85","version":"4.9.36","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2017-07-10T13:16:08"},{"id":"linode/4.9.33-x86_64-linode83","label":"4.9.33-x86_64-linode83","version":"4.9.33","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2017-06-23T21:04:33"},{"id":"linode/4.9.15-x86_64-linode81","label":"4.9.15-x86_64-linode81","version":"4.9.15","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2017-03-22T13:49:33"},{"id":"linode/4.9.7-x86_64-linode80","label":"4.9.7-x86_64-linode80","version":"4.9.7","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2017-02-03T22:55:37"},{"id":"linode/4.9.0-x86_64-linode79","label":"4.9.0-x86_64-linode79","version":"4.9.0","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-12-13T20:11:03"},{"id":"linode/4.8.6-x86_64-linode78","label":"4.8.6-x86_64-linode78","version":"4.8.6","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-11-02T15:24:17"},{"id":"linode/4.8.4-x86_64-linode77","label":"4.8.4-x86_64-linode77","version":"4.8.4","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-10-27T18:53:07"},{"id":"linode/4.8.3-x86_64-linode76","label":"4.8.3-x86_64-linode76","version":"4.8.3","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-10-20T23:10:27"},{"id":"linode/4.8.1-x86_64-linode75","label":"4.8.1-x86_64-linode75","version":"4.8.1","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-10-07T22:22:13"},{"id":"linode/4.7.3-x86_64-linode73","label":"4.7.3-x86_64-linode73","version":"4.7.3","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-09-15T13:13:01"},{"id":"linode/4.7.0-x86_64-linode72","label":"4.7.0-x86_64-linode72","version":"4.7.0","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-08-05T14:34:25"},{"id":"linode/4.6.5-x86_64-linode71","label":"4.6.5-x86_64-linode71","version":"4.6.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-08-04T15:28:01"},{"id":"linode/4.6.3-x86_64-linode70","label":"4.6.3-x86_64-linode70","version":"4.6.3","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-07-07T22:08:28"},{"id":"linode/4.5.5-x86_64-linode69","label":"4.5.5-x86_64-linode69","version":"4.5.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-05-24T15:30:08"},{"id":"linode/4.5.3-x86_64-linode67","label":"4.5.3-x86_64-linode67","version":"4.5.3","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-05-10T19:42:43"},{"id":"linode/4.5.0-x86_64-linode65","label":"4.5.0-x86_64-linode65","version":"4.5.0","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-03-16T18:53:02"},{"id":"linode/4.4.4-x86_64-linode64","label":"4.4.4-x86_64-linode64","version":"4.4.4","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-03-10T22:24:51"},{"id":"linode/4.4.0-x86_64-linode63","label":"4.4.0-x86_64-linode63","version":"4.4.0","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2016-01-20T14:41:05"},{"id":"linode/4.1.5-x86_64-linode61","label":"4.1.5-x86_64-linode61","version":"4.1.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-08-24T19:00:43"},{"id":"linode/4.1.5-x86_64-linode60","label":"4.1.5-x86_64-linode60 ","version":"4.1.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-08-13T13:00:00"},{"id":"linode/4.1.0-x86_64-linode59","label":"4.1.0-x86_64-linode59 ","version":"4.1.0","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-06-22T15:19:32"},{"id":"linode/4.0.5-x86_64-linode58","label":"4.0.5-x86_64-linode58","version":"4.0.5","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-06-10T15:31:52"},{"id":"linode/4.0.4-x86_64-linode57","label":"4.0.4-x86_64-linode57","version":"4.0.4","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-05-21T15:15:47"},{"id":"linode/4.0.2-x86_64-linode56","label":"4.0.2-x86_64-linode56","version":"4.0.2","kvm":true,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-05-11T20:56:58"},{"id":"linode/4.0.1-x86_64-linode55","label":"4.0.1-x86_64-linode55","version":"4.0.1","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-05-04T13:43:23"},{"id":"linode/4.0-x86_64-linode54","label":"4.0-x86_64-linode54","version":"4.0","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-04-21T13:02:24"},{"id":"linode/3.19.1-x86_64-linode53","label":"3.19.1-x86_64-linode53","version":"3.19.1","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-03-11T18:00:36"},{"id":"linode/3.18.5-x86_64-linode52","label":"3.18.5-x86_64-linode52","version":"3.18.5","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-02-05T05:00:00"},{"id":"linode/3.18.3-x86_64-linode51","label":"3.18.3-x86_64-linode51","version":"3.18.3","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-01-23T15:12:45"},{"id":"linode/3.18.1-x86_64-linode50","label":"3.18.1-x86_64-linode50","version":"3.18.1","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2015-01-06T17:32:39"},{"id":"linode/3.16.7-x86_64-linode49","label":"3.16.7-x86_64-linode49","version":"3.16.7","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-11-14T22:31:46"},{"id":"linode/3.16.5-x86_64-linode46","label":"3.16.5-x86_64-linode46","version":"3.16.5","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-10-13T13:42:00"},{"id":"linode/3.15.4-x86_64-linode45","label":"3.15.4-x86_64-linode45","version":"3.15.4","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-07-07T13:50:35"},{"id":"linode/3.15.3-x86_64-linode44","label":"3.15.3-x86_64-linode44","version":"3.15.3","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-07-02T12:12:37"},{"id":"linode/3.15.2-x86_64-linode43","label":"3.15.2-x86_64-linode43","version":"3.15.2","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-06-30T18:46:50"},{"id":"linode/3.14.5-x86_64-linode42","label":"3.14.5-x86_64-linode42","version":"3.14.5","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-06-05T20:05:44"},{"id":"linode/3.14.5-x86_64-linode41","label":"3.14.5-x86_64-linode41","version":"3.14.5","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-06-03T13:09:58"},{"id":"linode/3.14.4-x86_64-linode40","label":"3.14.4-x86_64-linode40","version":"3.14.4","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-05-13T17:42:22"},{"id":"linode/3.14.1-x86_64-linode39","label":"3.14.1-x86_64-linode39","version":"3.14.1","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-04-25T17:42:13"},{"id":"linode/3.13.7-x86_64-linode38","label":"3.13.7-x86_64-linode38","version":"3.13.7","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-03-25T18:21:50"},{"id":"linode/3.12.9-x86_64-linode37","label":"3.12.9-x86_64-linode37","version":"3.12.9","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2014-02-03T19:42:13"},{"id":"linode/3.12.6-x86_64-linode36","label":"3.12.6-x86_64-linode36","version":"3.12.6","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2013-12-23T16:24:18"},{"id":"linode/3.11.6-x86_64-linode35","label":"3.11.6-x86_64-linode35","version":"3.11.6","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2013-10-23T16:06:29"},{"id":"linode/3.10.3-x86_64-linode34","label":"3.10.3-x86_64-linode34","version":"3.10.3","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2013-07-26T16:35:12"},{"id":"linode/3.9.3-x86_64-linode33","label":"3.9.3-x86_64-linode33","version":"3.9.3","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2013-05-20T14:27:27"},{"id":"linode/3.9.2-x86_64-linode32","label":"3.9.2-x86_64-linode32","version":"3.9.2","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2013-05-14T15:53:02"},{"id":"linode/3.8.4-x86_64-linode31","label":"3.8.4-x86_64-linode31","version":"3.8.4","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2013-03-25T20:42:49"},{"id":"linode/3.7.10-x86_64-linode30","label":"3.7.10-x86_64-linode30","version":"3.7.10","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2013-02-27T19:49:45"},{"id":"linode/3.7.5-x86_64-linode29","label":"3.7.5-x86_64-linode29","version":"3.7.5","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2013-01-31T19:52:25"},{"id":"linode/3.6.5-x86_64-linode28","label":"3.6.5-x86_64-linode28","version":"3.6.5","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2012-11-04T17:42:14"},{"id":"linode/3.5.3-x86_64-linode27","label":"3.5.3-x86_64-linode27","version":"3.5.3","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2012-09-05T20:32:28"},{"id":"linode/3.4.2-x86_64-linode25","label":"3.4.2-x86_64-linode25","version":"3.2.4","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2012-06-11T18:40:20"},{"id":"linode/3.0.18-x86_64-linode24","label":"3.0.18-x86_64-linode24 ","version":"3.0.18","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2012-01-30T17:42:21"},{"id":"linode/3.2.1-x86_64-linode23","label":"3.2.1-x86_64-linode23","version":"3.2.0","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2012-01-23T16:04:48"},{"id":"linode/3.1.0-x86_64-linode22","label":"3.1.0-x86_64-linode22","version":"3.1.0","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2011-10-25T18:24:49"},{"id":"linode/3.0.4-x86_64-linode21","label":"3.0.4-x86_64-linode21","version":"3.0.4","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2011-09-02T01:08:55"},{"id":"linode/3.0.0-x86_64-linode20","label":"3.0.0-x86_64-linode20","version":"3.0.0","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2011-08-02T16:59:12"},{"id":"linode/2.6.39.1-x86_64-linode19","label":"2.6.39.1-x86_64-linode19","version":"2.6.39","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2011-06-21T14:06:03"},{"id":"linode/2.6.39-x86_64-linode18","label":"2.6.39-x86_64-linode18","version":"2.6.39","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2011-05-25T19:05:05"},{"id":"linode/2.6.38-x86_64-linode17","label":"2.6.38-x86_64-linode17","version":"2.6.38","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2011-03-21T21:44:09"},{"id":"linode/2.6.35.4-x86_64-linode16","label":"2.6.35.4-x86_64-linode16","version":"2.6.35","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2010-09-20T04:00:00"},{"id":"linode/2.6.32.12-x86_64-linode15","label":"2.6.32.12-x86_64-linode15","version":"2.6.32","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2010-07-25T21:34:00"},{"id":"linode/2.6.34-x86_64-linode13","label":"2.6.34-x86_64-linode13","version":"2.6.34","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2010-06-17T04:00:00"},{"id":"linode/2.6.34-x86_64-linode14","label":"2.6.34-x86_64-linode14","version":"2.6.34","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2010-07-14T04:00:00"},{"id":"linode/2.6.32.12-x86_64-linode12","label":"2.6.32.12-x86_64-linode12","version":"2.6.32","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2010-04-28T04:00:00"},{"id":"linode/2.6.32-x86_64-linode11","label":"2.6.32-x86_64-linode11","version":"2.6.32","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2009-12-05T17:01:00"},{"id":"linode/2.6.18.8-x86_64-linode10","label":"2.6.18.8-x86_64-linode10","version":"2.6.18","kvm":false,"architecture":"x86_64","pvops":false,"deprecated":true,"built":"2009-11-10T16:53:00"},{"id":"linode/2.6.31.5-x86_64-linode9","label":"2.6.31.5-x86_64-linode9","version":"2.6.31","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2009-10-26T04:00:00"},{"id":"linode/2.6.30.5-x86_64-linode8","label":"2.6.30.5-x86_64-linode8","version":"2.6.30","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2009-08-17T04:00:00"},{"id":"linode/2.6.18.8-x86_64-linode7","label":"2.6.18.8-x86_64-linode7","version":"2.6.18","kvm":false,"architecture":"x86_64","pvops":false,"deprecated":true,"built":"2009-08-14T04:00:00"},{"id":"linode/2.6.29-x86_64-linode6","label":"2.6.29-x86_64-linode6","version":"2.6.29","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2009-04-02T04:00:00"},{"id":"linode/2.6.28.3-x86_64-linode5","label":"2.6.28.3-x86_64-linode5","version":"2.6.28","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2009-02-04T05:00:00"},{"id":"linode/2.6.28-x86_64-linode4","label":"2.6.28-x86_64-linode4","version":"2.6.28","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2008-12-29T05:00:00"},{"id":"linode/2.6.27.4-x86_64-linode3","label":"2.6.27.4-x86_64-linode3","version":"2.6.27","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2008-11-05T05:00:00"},{"id":"linode/2.6.16.38-x86_64-linode2","label":"2.6.16.38-x86_64-linode2","version":"2.6.16","kvm":false,"architecture":"x86_64","pvops":false,"deprecated":true,"built":"2008-03-23T04:00:00"},{"id":"linode/2.6.18.8-x86_64-linode1","label":"2.6.18.8-x86_64-linode1","version":"2.6.18","kvm":false,"architecture":"x86_64","pvops":false,"deprecated":true,"built":"2008-03-23T04:00:00"},{"id":"linode/3.5.2-x86_64-linode26","label":"3.5.2-x86_64-linode26","version":"3.5.2","kvm":false,"architecture":"x86_64","pvops":true,"deprecated":true,"built":"2012-08-15T18:38:16"},{"id":"linode/grub2","label":"GRUB 2","version":"2.06","kvm":true,"architecture":"x86_64","pvops":false,"deprecated":false,"built":"2022-08-29T14:28:00"},{"id":"linode/direct-disk","label":"Direct Disk","version":"","kvm":true,"architecture":"x86_64","pvops":false,"deprecated":false,"built":"2015-05-05T01:51:43"},{"id":"linode/grub-legacy","label":"GRUB (Legacy)","version":"2.0.0","kvm":true,"architecture":"x86_64","pvops":false,"deprecated":false,"built":"2015-04-29T15:32:30"},{"id":"linode/pv-grub_x86_32","label":"pv-grub-x86_32","version":"2.6.26","kvm":false,"architecture":"i386","pvops":false,"deprecated":false,"built":"2008-09-15T04:00:00"},{"id":"linode/pv-grub_x86_64","label":"pv-grub-x86_64","version":"2.6.26","kvm":false,"architecture":"x86_64","pvops":false,"deprecated":false,"built":"2008-11-14T05:00:00"}],"page":1,"pages":1,"results":326} \ No newline at end of file diff --git a/packages/manager/src/cachedData/marketplace.json b/packages/manager/src/cachedData/marketplace.json deleted file mode 100644 index e5f2eb4151d..00000000000 --- a/packages/manager/src/cachedData/marketplace.json +++ /dev/null @@ -1 +0,0 @@ -{"data":[{"id":1146319,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"MongoDB Cluster Null One-Click","description":"MongoDB Cluster Null One-Click\r\nNull stackscript for 1067004","ordinal":0,"logo_url":"assets/mongodbmarketplaceocc.svg","images":["linode/ubuntu22.04"],"deployments_total":38,"deployments_active":0,"is_public":true,"mine":false,"created":"2023-03-23T14:00:01","updated":"2023-10-18T12:38:31","rev_note":"","script":"#!/bin/bash\n\n# Null","user_defined_fields":[]},{"id":1146324,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Galera Cluster Null One-Click","description":"Galera Cluster Null One-Click\r\nNull Stackscript for 1088136","ordinal":0,"logo_url":"assets/galeramarketplaceocc.svg","images":["linode/ubuntu22.04"],"deployments_total":166,"deployments_active":17,"is_public":true,"mine":false,"created":"2023-03-23T14:19:14","updated":"2024-01-10T18:38:51","rev_note":"","script":"#!/bin/bash","user_defined_fields":[]},{"id":1142293,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Redis Sentinel Cluster Null One-Click","description":"Redis Sentinel Cluster Null One-Click\r\nNull stackscript for 1132204","ordinal":0,"logo_url":"assets/redissentinelmarketplaceocc.svg","images":["linode/ubuntu22.04"],"deployments_total":175,"deployments_active":8,"is_public":true,"mine":false,"created":"2023-03-16T14:20:59","updated":"2023-12-17T23:45:42","rev_note":"","script":"#!/bin/bash","user_defined_fields":[]},{"id":1146322,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"PostgreSQL Cluster Null One-Click","description":"PostgreSQL Cluster Null One-Click\r\nNull Stackscript for 1068726","ordinal":0,"logo_url":"assets/postgresqlmarketplaceocc.svg","images":["linode/ubuntu22.04"],"deployments_total":244,"deployments_active":8,"is_public":true,"mine":false,"created":"2023-03-23T14:17:07","updated":"2024-01-10T14:06:08","rev_note":"","script":"#!/bin/bash\n\n# Null","user_defined_fields":[]},{"id":1226546,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"HashiCorp Nomad Cluster Null One-Click","description":"Nomad Cluster Null One-Click","ordinal":0,"logo_url":"assets/nomadocc.svg","images":["linode/ubuntu22.04"],"deployments_total":392,"deployments_active":16,"is_public":true,"mine":false,"created":"2023-08-25T19:08:21","updated":"2023-12-14T17:37:41","rev_note":"","script":"#!/bin/bash/","user_defined_fields":[]},{"id":1226547,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"HashiCorp Nomad Cluster Clients Null One-Click","description":"Nomad Cluster Clients One-Click","ordinal":0,"logo_url":"assets/nomadclientsocc.svg","images":["linode/ubuntu22.04"],"deployments_total":428,"deployments_active":24,"is_public":true,"mine":false,"created":"2023-08-25T19:08:57","updated":"2023-12-14T17:40:08","rev_note":"","script":"#!/bin/bash","user_defined_fields":[]},{"id":401697,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"WordPress One-Click","description":"Wordpress One Click App","ordinal":1,"logo_url":"assets/WordPress.svg","images":["linode/ubuntu22.04"],"deployments_total":66359,"deployments_active":4748,"is_public":true,"mine":false,"created":"2019-03-08T21:04:07","updated":"2024-01-10T21:35:15","rev_note":"","script":"#!/bin/bash\nset -e\ntrap \"cleanup $? $LINENO\" EXIT\n\n## Wordpress Settings\n#\n#\n\n#\n#\n#\n#\n\n## Linode/SSH Security Settings\n#\n#\n#\n#\n\n## Domain Settings\n#\n#\n#\n\n# git repo\nexport GIT_REPO=\"https://github.com/akamai-compute-marketplace/marketplace-apps.git\"\nexport WORK_DIR=\"/tmp/marketplace-apps\"\nexport MARKETPLACE_APP=\"apps/linode-marketplace-wordpress\"\n\n# enable logging\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n\nfunction cleanup {\n if [ -d \"${WORK_DIR}\" ]; then\n rm -rf ${WORK_DIR}\n fi\n\n}\n\nfunction udf {\n local group_vars=\"${WORK_DIR}/${MARKETPLACE_APP}/group_vars/linode/vars\"\n local web_stack=$(echo ${WEBSERVER_STACK} | tr [:upper:] [:lower:])\n sed 's/ //g' < ${group_vars}\n\n # deployment vars\n soa_email_address: ${SOA_EMAIL_ADDRESS}\n webserver_stack: ${web_stack}\n site_title: ${SITE_TITLE}\n wp_admin_user: ${WP_ADMIN_USER}\n wp_db_user: ${WP_DB_USER}\n wp_db_name: ${WP_DB_NAME}\nEOF\n\n if [[ -n ${USER_NAME} ]]; then\n echo \"username: ${USER_NAME}\" >> ${group_vars};\n else echo \"No username entered\";\n fi\n\n if [[ -n ${PASSWORD} ]]; then\n echo \"password: ${PASSWORD}\" >> ${group_vars};\n else echo \"No password entered\";\n fi\n\n if [[ -n ${PUBKEY} ]]; then\n echo \"pubkey: ${PUBKEY}\" >> ${group_vars};\n else echo \"No pubkey entered\";\n fi\n\n if [ \"$DISABLE_ROOT\" = \"Yes\" ]; then\n echo \"disable_root: yes\" >> ${group_vars};\n else echo \"Leaving root login enabled\";\n fi\n\n if [[ -n ${TOKEN_PASSWORD} ]]; then\n echo \"token_password: ${TOKEN_PASSWORD}\" >> ${group_vars};\n else echo \"No API token entered\";\n fi\n\n if [[ -n ${DOMAIN} ]]; then\n echo \"domain: ${DOMAIN}\" >> ${group_vars};\n #else echo \"No domain entered\";\n else echo \"default_dns: $(hostname -I | awk '{print $1}'| tr '.' '-' | awk {'print $1 \".ip.linodeusercontent.com\"'})\" >> ${group_vars};\n fi\n\n if [[ -n ${SUBDOMAIN} ]]; then\n echo \"subdomain: ${SUBDOMAIN}\" >> ${group_vars};\n else echo \"subdomain: www\" >> ${group_vars};\n fi\n}\n\nfunction run {\n # install dependancies\n apt-get update\n apt-get install -y git python3 python3-pip\n\n # clone repo and set up ansible environment\n git -C /tmp clone ${GIT_REPO}\n # for a single testing branch\n # git -C /tmp clone -b ${BRANCH} ${GIT_REPO}\n\n # venv\n cd ${WORK_DIR}/${MARKETPLACE_APP}\n pip3 install virtualenv\n python3 -m virtualenv env\n source env/bin/activate\n pip install pip --upgrade\n pip install -r requirements.txt\n ansible-galaxy install -r collections.yml\n\n # populate group_vars\n udf\n # run playbooks\n for playbook in provision.yml site.yml; do ansible-playbook -v $playbook; done\n}\n\nfunction installation_complete {\n # dumping credentials\n egrep \"(*^wp_|*mysql)\" ${WORK_DIR}/${MARKETPLACE_APP}/group_vars/linode/vars | awk {'print $1 $2'} > /root/.linode_credentials.txt\n cat << EOF\n#########################\n# INSTALLATION COMPLETE #\n############################################\n# The Mysql root password can be found at: #\n# - /root/.linode_credentials.txt #\n# #\n# * Hugs are worth more than handshakes * #\n############################################\nEOF\n}\n# main\nrun && installation_complete\ncleanup","user_defined_fields":[{"name":"soa_email_address","label":"Email address (for the Let's Encrypt SSL certificate)","example":"user@domain.tld"},{"name":"webserver_stack","label":"The stack you are looking to deploy Wordpress on","oneof":"LAMP,LEMP"},{"name":"site_title","label":"Website title","example":"My Blog"},{"name":"wp_admin_user","label":"Admin username","example":"admin"},{"name":"wp_db_user","label":"Wordpress database user","example":"wordpress"},{"name":"wp_db_name","label":"Wordpress database name","example":"wordpress"},{"name":"user_name","label":"The limited sudo user to be created for the Linode","default":""},{"name":"password","label":"The password for the limited sudo user","example":"an0th3r_s3cure_p4ssw0rd","default":""},{"name":"disable_root","label":"Disable root access over SSH?","oneof":"Yes,No","default":"No"},{"name":"pubkey","label":"The SSH Public Key that will be used to access the Linode (Recommended)","default":""},{"name":"token_password","label":"Your Linode API token. This is needed to create your Linode's DNS records","default":""},{"name":"subdomain","label":"Subdomain","example":"The subdomain for the DNS record. `www` will be entered if no subdomain is supplied (Requires Domain)","default":""},{"name":"domain","label":"Domain","example":"The domain for the DNS record: example.com (Requires API token)","default":""}]},{"id":632758,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Nextcloud One-Click","description":"One Click App - Nextcloud","ordinal":2,"logo_url":"assets/nextcloud.svg","images":["linode/ubuntu22.04"],"deployments_total":19420,"deployments_active":825,"is_public":true,"mine":false,"created":"2020-02-18T16:40:45","updated":"2024-01-10T15:11:14","rev_note":"","script":"#!/bin/bash\nset -e\ntrap \"cleanup $? $LINENO\" EXIT\n\n##Linode/SSH security settings\n#\n#\n#\n#\n\n## Domain Settings\n#\n#\n#\n\n## harbor Settings \n#\n\n# git repo\nexport GIT_REPO=\"https://github.com/akamai-compute-marketplace/marketplace-apps.git\"\nexport WORK_DIR=\"/tmp/marketplace-apps\" \nexport MARKETPLACE_APP=\"apps/linode-marketplace-nextcloud\"\n\n# enable logging\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n\nfunction cleanup {\n if [ -d \"${WORK_DIR}\" ]; then\n rm -rf ${WORK_DIR}\n fi\n\n}\n\nfunction udf {\n local group_vars=\"${WORK_DIR}/${MARKETPLACE_APP}/group_vars/linode/vars\"\n \n if [[ -n ${USER_NAME} ]]; then\n echo \"username: ${USER_NAME}\" >> ${group_vars};\n else echo \"No username entered\";\n fi\n\n if [ \"$DISABLE_ROOT\" = \"Yes\" ]; then\n echo \"disable_root: yes\" >> ${group_vars};\n else echo \"Leaving root login enabled\";\n fi\n\n if [[ -n ${PASSWORD} ]]; then\n echo \"password: ${PASSWORD}\" >> ${group_vars};\n else echo \"No password entered\";\n fi\n\n if [[ -n ${PUBKEY} ]]; then\n echo \"pubkey: ${PUBKEY}\" >> ${group_vars};\n else echo \"No pubkey entered\";\n fi\n\n # nextcloud vars\n \n if [[ -n ${SOA_EMAIL_ADDRESS} ]]; then\n echo \"soa_email_address: ${SOA_EMAIL_ADDRESS}\" >> ${group_vars};\n fi\n\n if [[ -n ${DOMAIN} ]]; then\n echo \"domain: ${DOMAIN}\" >> ${group_vars};\n else\n echo \"default_dns: $(hostname -I | awk '{print $1}'| tr '.' '-' | awk {'print $1 \".ip.linodeusercontent.com\"'})\" >> ${group_vars};\n fi\n\n if [[ -n ${SUBDOMAIN} ]]; then\n echo \"subdomain: ${SUBDOMAIN}\" >> ${group_vars};\n else echo \"subdomain: www\" >> ${group_vars};\n fi\n \n if [[ -n ${TOKEN_PASSWORD} ]]; then\n echo \"token_password: ${TOKEN_PASSWORD}\" >> ${group_vars};\n else echo \"No API token entered\";\n fi\n\n}\n\nfunction run {\n # install dependancies\n apt-get update\n apt-get install -y git python3 python3-pip\n\n # clone repo and set up ansible environment\n git -C /tmp clone ${GIT_REPO}\n # for a single testing branch\n # git -C /tmp clone --single-branch --branch ${BRANCH} ${GIT_REPO}\n\n # venv\n cd ${WORK_DIR}/${MARKETPLACE_APP}\n pip3 install virtualenv\n python3 -m virtualenv env\n source env/bin/activate\n pip install pip --upgrade\n pip install -r requirements.txt\n ansible-galaxy install -r collections.yml\n \n\n # populate group_vars\n udf\n # run playbooks\n for playbook in site.yml; do ansible-playbook -v $playbook; done\n \n}\n\nfunction installation_complete {\n echo \"Installation Complete\"\n}\n# main\nrun && installation_complete\ncleanup","user_defined_fields":[{"name":"user_name","label":"The limited sudo user to be created for the Linode","default":""},{"name":"password","label":"The password for the limited sudo user","example":"an0th3r_s3cure_p4ssw0rd","default":""},{"name":"disable_root","label":"Disable root access over SSH?","oneof":"Yes,No","default":"No"},{"name":"pubkey","label":"The SSH Public Key that will be used to access the Linode (Recommended)","default":""},{"name":"token_password","label":"Your Linode API token. This is needed to create your server's DNS records","default":""},{"name":"subdomain","label":"Subdomain","example":"The subdomain for the DNS record: www (Requires Domain)","default":""},{"name":"domain","label":"Domain","example":"The domain for the DNS record: example.com (Requires API token)","default":""},{"name":"soa_email_address","label":"email DNS record SOA","default":""}]},{"id":1017300,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Kali Linux One-Click","description":"Kali Linux One-Click","ordinal":3,"logo_url":"assets/kalilinux.svg","images":["linode/kali"],"deployments_total":17398,"deployments_active":458,"is_public":true,"mine":false,"created":"2022-06-21T14:38:37","updated":"2024-01-10T20:55:33","rev_note":"","script":"#!/bin/bash\n## Kali\n#\n#\n#\n#\n#\n\n## Linode/SSH Security Settings\n#\n#\n\n## Domain Settings\n#\n#\n#\n#\n\n## Enable logging\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\nset -o pipefail\n\n# Source the Linode Bash StackScript, API, and OCA Helper libraries\nsource \nsource \nsource \n\n# Source and run the New Linode Setup script for DNS/SSH configuration\nsource \n\nfunction headlessoreverything {\n if [ $HEADLESS == \"Yes\" ] && [ $EVERYTHING == \"Yes\" ]; then \n DEBIAN_FRONTEND=noninteractive apt-get install kali-linux-everything -y -yq -o Dpkg::Options::=\"--force-confdef\" -o Dpkg::Options::=\"--force-confold\"\n elif [ $EVERYTHING == \"Yes\" ] && [ $HEADLESS == \"No\" ]; then\n DEBIAN_FRONTEND=noninteractive apt-get install kali-linux-everything -y -yq -o Dpkg::Options::=\"--force-confdef\" -o Dpkg::Options::=\"--force-confold\"\n elif [ $HEADLESS == \"Yes\" ] && [ $EVERYTHING == \"No\" ]; then \n DEBIAN_FRONTEND=noninteractive apt-get install kali-linux-headless -y -yq -o Dpkg::Options::=\"--force-confdef\" -o Dpkg::Options::=\"--force-confold\"\n elif [ $HEADLESS == \"No\" ] && [ $EVERYTHING == \"No\" ]; then \n echo \"No Package Selected\"\n fi\n}\n\nfunction vncsetup {\n if [ $VNC == \"Yes\" ]; then \n ## XFCE & VNC Config\n apt-get install xfce4 xfce4-goodies dbus-x11 tigervnc-standalone-server expect -y -yq -o Dpkg::Options::=\"--force-confdef\" -o Dpkg::Options::=\"--force-confold\"\n\n readonly VNCSERVER_SET_PASSWORD=$(expect -c \"\nspawn sudo -u $USERNAME vncserver\nexpect \\\"Password:\\\"\nsend \\\"$PASSWORD\\r\\\"\nexpect \\\"Verify:\\\"\nsend \\\"$PASSWORD\\r\\\"\nexpect \\\"Would you like to enter a view-only password (y/n)?\\\"\nsend \\\"n\\r\\\"\nexpect eof\n\")\necho \"$VNCSERVER_SET_PASSWORD\"\n sleep 2\n killvncprocess=$(ps aux | grep \"/usr/bin/Xtigervnc :1 -localhost=1 -desktop\" | head -n 1 | awk '{ print $2; }')\n kill $killvncprocess\n touch /etc/systemd/system/vncserver@.service\n cat < /etc/systemd/system/vncserver@.service\n[Unit]\nDescription=a wrapper to launch an X server for VNC\nAfter=syslog.target network.target\n[Service]\nType=forking\nUser=$USERNAME\nGroup=$USERNAME\nWorkingDirectory=/home/$USERNAME\nExecStartPre=-/usr/bin/vncserver -kill :%i > /dev/null 2>&1\nExecStart=/usr/bin/vncserver -depth 24 -geometry 1280x800 -localhost :%i\nExecStop=/usr/bin/vncserver -kill :%i\n[Install]\nWantedBy=multi-user.target\nEOF\n systemctl daemon-reload\n systemctl start vncserver@1.service\n systemctl enable vncserver@1.service\n\n cat < /etc/motd\n###################################\n# VNC SSH Tunnel Instructions #\n###################################\n\n* Ensure you have a VNC Client installed on your local machine\n* Run the command below to start the SSH tunnel for VNC \n\n ssh -L 61000:localhost:5901 -N -l $USERNAME $FQDN\n\n* For more Detailed documentation please visit the offical Documentation below\n\n https://www.linode.com/docs/products/tools/marketplace/guides/kalilinux\n\n### To remove this message, you can edit the /etc/motd file ###\nEOF\n fi\n}\n\nfunction main {\n headlessoreverything\n vncsetup\n stackscript_cleanup\n}\n\nmain","user_defined_fields":[{"name":"everything","label":"Would you like to Install the Kali Everything Package?","oneof":"Yes,No","default":"Yes"},{"name":"headless","label":"Would you like to Install the Kali Headless Package?","oneof":"Yes,No","default":"No"},{"name":"vnc","label":"Would you like to setup VNC to access Kali XFCE Desktop","oneof":"Yes,No","default":"Yes"},{"name":"username","label":"The VNC user to be created for the Linode. The username accepts only lowercase letters, numbers, dashes (-) and underscores (_)"},{"name":"password","label":"The password for the limited VNC user"},{"name":"pubkey","label":"The SSH Public Key that will be used to access the Linode","default":""},{"name":"disable_root","label":"Disable root access over SSH?","oneof":"Yes,No","default":"No"},{"name":"token_password","label":"Your Linode API token. This is required for creating DNS records.","default":""},{"name":"subdomain","label":"The subdomain for the Linode's DNS record (Requires API token)","default":""},{"name":"domain","label":"The domain for the Linode's DNS record (Requires API token)","default":""},{"name":"soa_email_address","label":"Email address for SOA records (Requires API token)","default":""}]},{"id":593835,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Plesk One-Click","description":"Plesk is the leading secure WordPress and website management platform. This Stackscript installs the latest publicly available Plesk, activates a trial license, installs essential extensions, and sets up and configures the firewall. Please allow the script around 15 minutes to finish.","ordinal":4,"logo_url":"assets/plesk.svg","images":["linode/centos7","linode/ubuntu20.04"],"deployments_total":10673,"deployments_active":494,"is_public":true,"mine":false,"created":"2019-09-26T17:34:17","updated":"2024-01-10T20:49:03","rev_note":"updated wording","script":"#!/bin/bash\n# This block defines the variables the user of the script needs to input\n# when deploying using this script.\n#\n## Enable logging\nset -xo pipefail\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n## Import the Bash StackScript Library\nsource \n## Import the DNS/API Functions Library\nsource \n## Import the OCA Helper Functions\nsource \n## Run initial configuration tasks (DNS/SSH stuff, etc...)\nsource \n\nfunction pleskautoinstall {\n echo \"Downloading Plesk Auto-Installer\"\n sh <(curl https://autoinstall.plesk.com/one-click-installer || wget -O - https://autoinstall.plesk.com/one-click-installer)\n echo \"turning on http2\"\n /usr/sbin/plesk bin http2_pref --enable\n}\n\nfunction firewall {\n echo \"Setting Firewall to allow proper ports.\"\n if [ \"${detected_distro[distro]}\" = 'centos' ]; then \n iptables -I INPUT -p tcp --dport 21 -j ACCEPT\n iptables -I INPUT -p tcp --dport 22 -j ACCEPT\n iptables -I INPUT -p tcp --dport 25 -j ACCEPT\n iptables -I INPUT -p tcp --dport 80 -j ACCEPT\n iptables -I INPUT -p tcp --dport 110 -j ACCEPT\n iptables -I INPUT -p tcp --dport 143 -j ACCEPT\n iptables -I INPUT -p tcp --dport 443 -j ACCEPT\n iptables -I INPUT -p tcp --dport 465 -j ACCEPT\n iptables -I INPUT -p tcp --dport 993 -j ACCEPT\n iptables -I INPUT -p tcp --dport 995 -j ACCEPT\n iptables -I INPUT -p tcp --dport 8443 -j ACCEPT\n iptables -I INPUT -p tcp --dport 8447 -j ACCEPT\n iptables -I INPUT -p tcp --dport 8880 -j ACCEPT\n elif [ \"${detected_distro[distro]}\" = 'ubuntu' ]; then\n ufw allow 21\n ufw allow 22\n ufw allow 25\n ufw allow 80\n ufw allow 110\n ufw allow 143\n ufw allow 443\n ufw allow 465\n ufw allow 993\n ufw allow 995\n ufw allow 8443\n ufw allow 8447\n ufw allow 8880\nelse \necho \"Distro Not supported\"\nfi\n}\n\nfunction main {\n pleskautoinstall\n firewall\n}\n\n# Execute script\nsystem_update\nmain\nstackscript_cleanup","user_defined_fields":[]},{"id":595742,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"cPanel One-Click","description":"cPanel One-Click","ordinal":5,"logo_url":"assets/cpanel.svg","images":["linode/almalinux8","linode/rocky8"],"deployments_total":28549,"deployments_active":979,"is_public":true,"mine":false,"created":"2019-09-30T20:17:52","updated":"2024-01-10T20:44:43","rev_note":"","script":"#!/bin/bash\nset -e\n\n# Commit: fde6587e08ea95321ce010e52a9c1b8d02455a97\n# Commit date: 2023-02-13 17:00:46 -0600\n# Generated: 2023-02-17 11:00:28 -0600\n\n# Add Logging to /var/log/stackscript.log for future troubleshooting\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n\necho $(date +%Y%m%d%H%M%S) >> /tmp/cpdebug.log\n\n# Linode's Weblish console will truncate lines unless you do this tput smam. This\n# instructs the terminal to wrap your lines, which is especially important so that\n# the WHM login URL that gets printed at the end can be copied.\ntput smam\n\nsource /etc/os-release\n\nis_os_and_version_id_prefix() {\n [[ $ID == $1 ]] && [[ $VERSION_ID =~ ^$2 ]]\n}\n\nis_almalinux8() {\n is_os_and_version_id_prefix almalinux 8\n}\n\nis_centos7() {\n is_os_and_version_id_prefix centos 7\n}\n\nis_cloudlinux7() {\n is_os_and_version_id_prefix cloudlinux 7\n}\n\nis_cloudlinux8() {\n is_os_and_version_id_prefix cloudlinux 8\n}\n\nis_rocky8() {\n is_os_and_version_id_prefix rocky 8\n}\n\nis_ubuntu20() {\n is_os_and_version_id_prefix ubuntu 20.04\n}\n\nis_supported_os() {\n is_almalinux8 || \\\n is_centos7 || \\\n is_cloudlinux7 || \\\n is_cloudlinux8 || \\\n is_rocky8 || \\\n is_ubuntu20\n}\n\nhas_yum() {\n which yum >/dev/null 2>&1\n}\n\nhas_dnf() {\n which dnf >/dev/null 2>&1\n}\n\nhas_apt() {\n which apt >/dev/null 2>&1\n}\n\nis_networkmanager_enabled() {\n systemctl is-enabled NetworkManager.service > /dev/null 2>&1\n}\n\n# cPanel & WHM is incompatible with NetworkManager\nif is_networkmanager_enabled; then\n systemctl stop NetworkManager.service\n systemctl disable NetworkManager.service\n if has_dnf; then\n dnf -y remove NetworkManager\n elif has_yum; then\n yum -y remove NetworkManager\n fi\nfi\n\nhostnamectl set-hostname server.hostname.tld\n\ncd /home && curl -so installer -L https://securedownloads.cpanel.net/latest\n\nif is_supported_os; then\n if is_ubuntu20; then\n apt-get -o Acquire::ForceIPv4=true update -y\n DEBIAN_FRONTEND=noninteractive apt-get -y -o DPkg::options::=\"--force-confdef\" -o DPkg::options::=\"--force-confold\" install grub-pc\n sh installer --skiplicensecheck --skip-cloudlinux\n else\n sh installer --skiplicensecheck\n fi\nelse\n echo \"Your distribution is not supported by this StackScript.\"\n install -d -v -m 711 /var/cpanel\n touch /var/cpanel/cpinit.failed\nfi\n\nrm -f /etc/cpupdate.conf\ncat > /root/.bash_profile <<'END_OF_BASH_PROFILE'\n# .bash_profile\n# Get the aliases and functions\nif [ -f ~/.bashrc ]; then\n . ~/.bashrc\nfi\n# User specific environment and startup programs\nPATH=$PATH:$HOME/bin\nexport PATH\nbash /etc/motd.sh\nif [ -t 0 ]; then\n URL=`whmlogin --nowait 2> /dev/null`\n WHMLOGIN_RETURN=$?\n if [ $WHMLOGIN_RETURN == 1 ]; then\n # whmlogin doesn't support --nowait. Output a URL and hope it's accurate.\n echo \"To log in to WHM as the root user, visit the following address in your web browser:\"\n echo \"\"\n whmlogin\n echo \"\"\n echo \"Thank you for using cPanel & WHM!\"\n else\n if [ $WHMLOGIN_RETURN == 2 ]; then\n # whmlogin indicates that cpinit hasn't updated the IP/hostname yet.\n echo \"To log in to WHM as the root user, run the command 'whmlogin' to get a web address for your browser.\"\n echo \"\"\n echo \"Thank you for using cPanel & WHM!\"\n else\n # whmlogin returned a valid URL to use.\n echo \"To log in to WHM as the root user, visit the following address in your web browser:\"\n echo \"\"\n echo \"$URL\"\n echo \"\"\n echo \"Thank you for using cPanel & WHM!\"\n fi\n fi\nfi\nEND_OF_BASH_PROFILE\n\ncat > /etc/motd.sh <<'END_OF_MOTD'\n#!/bin/bash\nsource /etc/os-release\necho \"\n ____ _ ___ __ ___ _ __ __\n ___| _ \\ __ _ _ __ ___| | ( _ ) \\ \\ / / | | | \\/ |\n / __| |_) / _. | ._ \\ / _ \\ | / _ \\/\\ \\ \\ /\\ / /| |_| | |\\/| |\n| (__| __/ (_| | | | | __/ | | (_> < \\ V V / | _ | | | |\n \\___|_| \\__._|_| |_|\\___|_| \\___/\\/ \\_/\\_/ |_| |_|_| |_|\n\"\necho \"Welcome to cPanel & WHM `/usr/local/cpanel/cpanel -V`\"\necho \"\"\necho \"Running $PRETTY_NAME\"\necho \"\"\necho \"For our full cPanel & WHM documentation: https://go.cpanel.net/docs\"\necho \"\"\necho \"For information on how to quickly set up a website in cPanel & WHM: https://go.cpanel.net/buildasite\"\necho \"\" # This new line makes output from bash_profiles easier to read\nEND_OF_MOTD\ntouch /var/cpanel/cpinit.done","user_defined_fields":[]},{"id":691621,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Cloudron One-Click","description":"Cloudron One-Click","ordinal":6,"logo_url":"assets/cloudron.svg","images":["linode/ubuntu20.04"],"deployments_total":13962,"deployments_active":604,"is_public":true,"mine":false,"created":"2020-11-30T21:21:45","updated":"2024-01-10T19:35:46","rev_note":"","script":"#!/bin/bash\n\n# Add Logging to /var/log/stackscript.log for future troubleshooting\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n\n# apt-get updates\n echo 'Acquire::ForceIPv4 \"true\";' > /etc/apt/apt.conf.d/99force-ipv4\n export DEBIAN_FRONTEND=noninteractive\n apt-get update -y\n\nwget https://cloudron.io/cloudron-setup\nchmod +x cloudron-setup\n./cloudron-setup --provider linode-mp\n\necho All finished! Rebooting...\n(sleep 5; reboot) &","user_defined_fields":[]},{"id":692092,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Secure Your Server One-Click","description":"Secure Your Server One-Click","ordinal":7,"logo_url":"assets/secureyourserver.svg","images":["linode/debian10","linode/ubuntu20.04","linode/debian11","linode/ubuntu22.04"],"deployments_total":4896,"deployments_active":750,"is_public":true,"mine":false,"created":"2020-12-03T10:01:28","updated":"2024-01-10T20:16:31","rev_note":"","script":"#!/usr/bin/env bash\n\n## User and SSH Security\n#\n#\n#\n#\n\n## Domain\n#\n#\n#\n#\n#\n\n## Block Storage\n#\n#\n\n\n# Enable logging for the StackScript\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n\n# Source Linode Helpers\nsource \nsource \nsource \nsource \n\n# Cleanup\nstackscript_cleanup","user_defined_fields":[{"name":"username","label":"The limited sudo user to be created for the Linode. (lower case only)"},{"name":"password","label":"The password for the limited sudo user"},{"name":"pubkey","label":"The SSH Public Key that will be used to access the Linode"},{"name":"disable_root","label":"Would you like to disable root login over SSH?","oneof":"Yes,No"},{"name":"token_password","label":"Your Linode API token - This is required for creating DNS records","default":""},{"name":"domain","label":"The domain for the Linode's DNS record (Requires API token)","default":""},{"name":"subdomain","label":"The subdomain for the Linode's DNS record (Requires API token and domain)","default":""},{"name":"soa_email_address","label":"Your email address. This is used for creating DNS records and website VirtualHost configuration.","default":""},{"name":"send_email","label":"Would you like to be able to send email from this domain? (Requires domain)","oneof":"Yes,No","default":"No"},{"name":"volume","label":"To use a Block Storage volume, enter its name here.","default":""},{"name":"volume_size","label":"If creating a new Block Storage volume, enter its size in GB (NOTE: This creates a billable resource at $0.10/month per GB).","default":""}]},{"id":925722,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Pritunl One-Click","description":"Pritunl One-Click","ordinal":8,"logo_url":"assets/pritunl.svg","images":["linode/debian10","linode/ubuntu20.04"],"deployments_total":1210,"deployments_active":74,"is_public":true,"mine":false,"created":"2021-10-26T15:23:37","updated":"2024-01-09T18:08:12","rev_note":"","script":"#!/usr/bin/env bash\n\n## Linode/SSH Security Settings\n#\n#\n#\n#\n\n## Domain Settings\n#\n#\n#\n#\n\n## Enable logging\nset -o pipefail\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n## Import the Bash StackScript Library\nsource \n## Import the DNS/API Functions Library\nsource \n## Import the OCA Helper Functions\nsource \n## Run initial configuration tasks (DNS/SSH stuff, etc...)\nsource \n\n# Update system & set hostname & basic security\nset_hostname\napt_setup_update\nufw_install\nufw allow 443\nufw allow 80\nfail2ban_install\n\n# Mongo Install\napt-get install -y wget gnupg dirmngr \nwget -qO - https://www.mongodb.org/static/pgp/server-5.0.asc | sudo apt-key add -\nif [ \"${detected_distro[distro]}\" = 'debian' ]; then \necho \"deb http://repo.mongodb.org/apt/debian buster/mongodb-org/5.0 main\" | sudo tee /etc/apt/sources.list.d/mongodb-org-5.0.list\nelif [ \"${detected_distro[distro]}\" = 'ubuntu' ]; then\necho \"deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/5.0 multiverse\" | sudo tee /etc/apt/sources.list.d/mongodb-org-5.0.list\nelse \necho \"Setting this up for the future incase we add more distros\"\nfi\napt-get update -y\napt-get install -y mongodb-org\nsystemctl enable mongod.service\nsystemctl start mongod.service\n\n# Pritunl\napt-key adv --keyserver hkp://keyserver.ubuntu.com --recv E162F504A20CDF15827F718D4B7C549A058F8B6B\napt-key adv --keyserver hkp://keyserver.ubuntu.com --recv 7568D9BB55FF9E5287D586017AE645C0CF8E292A\nif [ \"${detected_distro[distro]}\" = 'debian' ]; then \necho \"deb http://repo.pritunl.com/stable/apt buster main\" | tee /etc/apt/sources.list.d/pritunl.list\nelif [ \"${detected_distro[distro]}\" = 'ubuntu' ]; then\necho \"deb http://repo.pritunl.com/stable/apt focal main\" | tee /etc/apt/sources.list.d/pritunl.list\nelse \necho \"Setting this up for the future incase we add more distros\"\nfi\n\napt update -y\napt install -y pritunl\n\nsystemctl enable pritunl.service\nsystemctl start pritunl.service\n\n# Performance tune\necho \"* hard nofile 64000\" >> /etc/security/limits.conf\necho \"* soft nofile 64000\" >> /etc/security/limits.conf\necho \"root hard nofile 64000\" >> /etc/security/limits.conf\necho \"root soft nofile 64000\" >> /etc/security/limits.conf\n\n# Cleanup\nstackscript_cleanup","user_defined_fields":[{"name":"username","label":"The limited sudo user to be created for the Linode","default":""},{"name":"password","label":"The password for the limited sudo user","example":"an0th3r_s3cure_p4ssw0rd","default":""},{"name":"pubkey","label":"The SSH Public Key that will be used to access the Linode","default":""},{"name":"disable_root","label":"Disable root access over SSH?","oneof":"Yes,No","default":"No"},{"name":"token_password","label":"Your Linode API token. This is needed to create your WordPress server's DNS records","default":""},{"name":"subdomain","label":"Subdomain","example":"The subdomain for the DNS record: www (Requires Domain)","default":""},{"name":"domain","label":"Domain","example":"The domain for the DNS record: example.com (Requires API token)","default":""},{"name":"soa_email_address","label":"Email address for the SOA record","default":""}]},{"id":741206,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"CyberPanel One-Click","description":"CyberPanel One-Click","ordinal":9,"logo_url":"assets/cyberpanel.svg","images":["linode/ubuntu20.04","linode/ubuntu22.04"],"deployments_total":11394,"deployments_active":580,"is_public":true,"mine":false,"created":"2021-01-27T02:46:19","updated":"2024-01-10T20:39:36","rev_note":"","script":"#!/bin/bash\n### linode\n## Enable logging\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n\n### Install cyberpanel\nbash <( curl -sk https://raw.githubusercontent.com/litespeedtech/ls-cloud-image/master/Setup/cybersetup.sh )\n\n### Regenerate password for Web Admin, Database, setup Welcome Message\nbash <( curl -sk https://raw.githubusercontent.com/litespeedtech/ls-cloud-image/master/Cloud-init/per-instance.sh )\n\n### Clean up ls tmp folder\nsudo rm -rf /tmp/lshttpd/*","user_defined_fields":[]},{"id":401709,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Minecraft: Java Edition One-Click","description":"Minecraft OCA","ordinal":10,"logo_url":"assets/Minecraft.svg","images":["linode/ubuntu20.04"],"deployments_total":20898,"deployments_active":331,"is_public":true,"mine":false,"created":"2019-03-08T21:13:32","updated":"2024-01-10T21:26:11","rev_note":"remove maxplayers hard coded options [oca-707]","script":"#!/usr/bin/env bash\n# Game config options:\n# https://minecraft.gamepedia.com/Server.properties\n#\n#\n#\n#\n#\n#\n#\n#\n#\n#\n#\n#\n#\n#\n#\n#\n#\n#\n#\n#\n#\n#\n#\n#\n#\n#\n#\n#\n#\n#\n#\n#\n## Linode/SSH Security Settings - Required\n#\n#\n## Linode/SSH Settings - Optional\n#\n#\n\n# Enable logging for the StackScript\nset -xo pipefail\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n\n# Source the Linode Bash StackScript, API, and LinuxGSM Helper libraries\nsource \nsource \nsource \n\n# Source and run the New Linode Setup script for DNS/SSH configuration\n[ ! $USERNAME ] && USERNAME='lgsmuser'\nsource \n\n# Difficulty\n[[ \"$DIFFICULTY\" = \"Peaceful\" ]] && DIFFICULTY=0\n[[ \"$DIFFICULTY\" = \"Easy\" ]] && DIFFICULTY=1\n[[ \"$DIFFICULTY\" = \"Normal\" ]] && DIFFICULTY=2\n[[ \"$DIFFICULTY\" = \"Hard\" ]] && DIFFICULTY=3\n\n# Gamemode\n[[ \"$GAMEMODE\" = \"Survival\" ]] && GAMEMODE=0\n[[ \"$GAMEMODE\" = \"Creative\" ]] && GAMEMODE=1\n[[ \"$GAMEMODE\" = \"Adventure\" ]] && GAMEMODE=2\n[[ \"$GAMEMODE\" = \"Spectator\" ]] && GAMEMODE=3\n\n# Player Idle Timeout\n[[ \"$PLAYERIDLETIMEOUT\" = \"Disabled\" ]] && PLAYERIDLETIMEOUT=0\n\n# Minecraft-specific dependencies\ndebconf-set-selections <<< \"postfix postfix/main_mailer_type string 'No Configuration'\"\ndebconf-set-selections <<< \"postfix postfix/mailname string `hostname`\"\ndpkg --add-architecture i386\nsystem_install_package mailutils postfix curl netcat wget file bzip2 \\\n gzip unzip bsdmainutils python util-linux ca-certificates \\\n binutils bc jq tmux openjdk-17-jre dirmngr software-properties-common\n\n# Install LinuxGSM and Minecraft and enable the 'mcserver' service\nreadonly GAMESERVER='mcserver'\nv_linuxgsm_oneclick_install \"$GAMESERVER\" \"$USERNAME\"\n\n# Minecraft configurations\nsed -i s/server-ip=/server-ip=\"$IP\"/ /home/\"$USERNAME\"/serverfiles/server.properties\n\n# Customer config\nsed -i s/allow-flight=false/allow-flight=\"$ALLOWFLIGHT\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/allow-nether=true/allow-nether=\"$ALLOWNETHER\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/announce-player-achievements=true/announce-player-achievements=\"$ANNOUNCEPLAYERACHIEVEMENTS\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/difficulty=1/difficulty=\"$DIFFICULTY\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/enable-command-block=false/enable-command-block=\"$ENABLECOMMANDBLOCK\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/enable-query=true/enable-query=\"$ENABLEQUERY\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/force-gamemode=false/force-gamemode=\"$FORCEGAMEMODE\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/gamemode=0/gamemode=\"$GAMEMODE\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/generate-structures=true/generate-structures=\"$GENERATESTRUCTURES\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/hardcore=false/hardcore=\"$HARDCORE\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/level-name=world/level-name=\"$LEVELNAME\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/level-seed=/level-seed=\"$LEVELSEED\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/level-type=DEFAULT/level-type=\"$LEVELTYPE\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/max-build-height=256/max-build-height=\"$MAXBUILDHEIGHT\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/max-players=20/max-players=\"$MAXPLAYERS\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/max-tick-time=60000/max-tick-time=\"$MAXTICKTIME\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/max-world-size=29999984/max-world-size=\"$MAXWORLDSIZE\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/motd=.*/motd=\"$MOTD\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/network-compression-threshold=256/network-compression-threshold=\"$NETWORKCOMPRESSIONTHRESHOLD\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/op-permission-level=4/op-permission-level=\"$OPPERMISSIONLEVEL\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/player-idle-timeout=0/player-idle-timeout=\"$PLAYERIDLETIMEOUT\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/pvp=true/pvp=\"$PVP\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/resource-pack-sha1=/resource-pack-sha1=\"$RESOURCEPACKSHA1\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/server-port=25565/server-port=\"$PORT\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/snooper-enabled=true/snooper-enabled=\"$SNOOPERENABLED\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/spawn-animals=true/spawn-animals=\"$SPAWNANIMALS\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/spawn-monsters=true/spawn-monsters=\"$SPAWNMONSTERS\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/spawn-npcs=true/spawn-npcs=\"$SPAWNNPCS\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/use-native-transport=true/use-native-transport=\"$USENATIVETRANSPORT\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/view-distance=10/view-distance=\"$VIEWDISTANCE\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/rcon.password=*/rcon.password=\"\\\"$RCONPASSWORD\\\"\"/ /home/\"$USERNAME\"/serverfiles/server.properties\nsed -i s/enable-rcon=false/enable-rcon=true/ /home/\"$USERNAME\"/serverfiles/server.properties\n\n# Start the service and setup firewall\nufw allow \"$PORT\"\nufw allow \"25575\"\n\n# Start and enable the Minecraft service\nsystemctl start \"$GAMESERVER\".service\nsystemctl enable \"$GAMESERVER\".service\n\n# Cleanup\nstackscript_cleanup","user_defined_fields":[{"name":"levelname","label":"World Name","default":"world"},{"name":"motd","label":"Message of the Day","default":"Powered by Linode!"},{"name":"allowflight","label":"Flight Enabled","oneof":"true,false","default":"false"},{"name":"allownether","label":"Nether World Enabled","oneof":"true,false","default":"true"},{"name":"announceplayerachievements","label":"Player Achievements Enabled","oneof":"true,false","default":"true"},{"name":"maxplayers","label":"Maximum Players","default":"25"},{"name":"playeridletimeout","label":"Player Idle Timeout Limit","oneof":"Disabled,15,30,45,60","default":"Disabled"},{"name":"difficulty","label":"Difficulty Level","oneof":"Peaceful,Easy,Normal,Hard","default":"Easy"},{"name":"hardcore","label":"Hardcore Mode Enabled","oneof":"true,false","default":"false"},{"name":"pvp","label":"PvP Enabled","oneof":"true,false","default":"true"},{"name":"forcegamemode","label":"Force Game Mode Enabled","oneof":"true,false","default":"false"},{"name":"leveltype","label":"World Type","oneof":"DEFAULT,AMPLIFIED,FLAT,LEGACY","default":"DEFAULT"},{"name":"levelseed","label":"World Seed","default":""},{"name":"spawnanimals","label":"Spawn Animals Enabled","oneof":"true,false","default":"true"},{"name":"spawnmonsters","label":"Spawn Monsters Enabled","oneof":"true,false","default":"true"},{"name":"spawnnpcs","label":"Spawn NPCs Enabled","oneof":"true,false","default":"true"},{"name":"gamemode","label":"Game Mode","oneof":"Survival,Creative,Adventure,Spectator","default":"Survival"},{"name":"generatestructures","label":"Structure Generation Enabled","oneof":"true,false","default":"true"},{"name":"maxbuildheight","label":"Maximum Build Height","oneof":"50,100,200,256","default":"256"},{"name":"maxworldsize","label":"Maximum World Size","oneof":"100,1000,10000,100000,1000000,10000000,29999984","default":"29999984"},{"name":"viewdistance","label":"View Distance","oneof":"2,5,10,15,25,32","default":"10"},{"name":"enablecommandblock","label":"Command Block Enabled","oneof":"true,false","default":"false"},{"name":"enablequery","label":"Querying Enabled","oneof":"true,false","default":"true"},{"name":"enablercon","label":"Enable RCON","oneof":"true,false","default":"false"},{"name":"rconpassword","label":"RCON Password","default":""},{"name":"rconport","label":"RCON Port","default":"25575"},{"name":"maxticktime","label":"Maximum Tick Time","default":"60000"},{"name":"networkcompressionthreshold","label":"Network Compression Threshold","default":"256"},{"name":"oppermissionlevel","label":"Op-permission Level","oneof":"1,2,3,4","default":"4"},{"name":"port","label":"Port Number","default":"25565"},{"name":"snooperenabled","label":"Snooper Enabled","oneof":"true,false","default":"true"},{"name":"usenativetransport","label":"Use Native Transport Enabled","oneof":"true,false","default":"true"},{"name":"username","label":"The username for the Linode's non-root admin/SSH user(must be lowercase)","example":"lgsmuser"},{"name":"password","label":"The password for the Linode's non-root admin/SSH user","example":"S3cuReP@s$w0rd"},{"name":"pubkey","label":"The SSH Public Key used to securely access the Linode via SSH","default":""},{"name":"disable_root","label":"Disable root access over SSH?","oneof":"Yes,No","default":"No"}]},{"id":869129,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"aaPanel One-Click","description":"aaPanel One-Click","ordinal":11,"logo_url":"assets/aapanel.svg","images":["linode/centos7"],"deployments_total":5510,"deployments_active":311,"is_public":true,"mine":false,"created":"2021-07-20T18:50:46","updated":"2024-01-10T09:12:41","rev_note":"","script":"#!/bin/bash\n\n# Enable logging for the StackScript\nset -xo pipefail\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n\n# Yum Update\nyum update -y\n\n# Install aapanel\nyum install -y wget && wget -O install.sh http://www.aapanel.com/script/install_6.0_en.sh && echo y|bash install.sh aapanel\n\n# Log aaPanel login information\nbt default > /root/.aapanel_info\n\n# Stackscript Cleanup\nrm /root/StackScript\nrm /root/ssinclude*\necho \"Installation complete!\"","user_defined_fields":[]},{"id":923033,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Akaunting One-Click","description":"Akaunting One-Click","ordinal":12,"logo_url":"assets/akaunting.svg","images":["linode/ubuntu22.04"],"deployments_total":658,"deployments_active":23,"is_public":true,"mine":false,"created":"2021-10-18T01:01:19","updated":"2024-01-09T13:24:38","rev_note":"","script":"#!/bin/bash\n\n# \n# \n# \n# \n\n# \n# \n# \n# \n\n# Add Logging to /var/log/stackscript.log for future troubleshooting\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n\nDEBIAN_FRONTEND=noninteractive apt-get update -qq >/dev/null\n\n###########################################################\n# Install NGINX\n###########################################################\napt-get install -y nginx\n\ncat <<'END' >/var/www/html/index.html\n\n \n \n \n \n \n\n Installing Akaunting\n\n \n \n\n \n \n\n \n \n \n \n \n\n \n \n \n \n Installing...Get back after 3 minutes!\n \n \n \n\nEND\n\nchown www-data:www-data /var/www/html/index.html\nchmod 644 /var/www/html/index.html\n\n###########################################################\n# MySQL\n###########################################################\napt install -y mariadb-server expect\n\nfunction mysql_secure_install {\n # $1 - required - Root password for the MySQL database\n [ ! -n \"$1\" ] && {\n printf \"mysql_secure_install() requires the MySQL database root password as its only argument\\n\"\n return 1;\n }\n local -r db_root_password=\"$1\"\n local -r secure_mysql=$(\nexpect -c \"\nset timeout 10\nspawn mysql_secure_installation\nexpect \\\"Enter current password for root (enter for none):\\\"\nsend \\\"$db_root_password\\r\\\"\nexpect \\\"Change the root password?\\\"\nsend \\\"n\\r\\\"\nexpect \\\"Remove anonymous users?\\\"\nsend \\\"y\\r\\\"\nexpect \\\"Disallow root login remotely?\\\"\nsend \\\"y\\r\\\"\nexpect \\\"Remove test database and access to it?\\\"\nsend \\\"y\\r\\\"\nexpect \\\"Reload privilege tables now?\\\"\nsend \\\"y\\r\\\"\nexpect eof\n\")\n printf \"$secure_mysql\\n\"\n}\n\n# Set DB root password\necho \"mysql-server mysql-server/root_password password ${DB_PASSWORD}\" | debconf-set-selections\necho \"mysql-server mysql-server/root_password_again password ${DB_PASSWORD}\" | debconf-set-selections\n\nmysql_secure_install \"$DB_PASSWORD\"\n\n# Create DB\necho \"CREATE DATABASE ${DB_NAME};\" | mysql -u root -p\"$DB_PASSWORD\"\n\n# create DB user with password\necho \"CREATE USER '$DBUSER'@'localhost' IDENTIFIED BY '$DBUSER_PASSWORD';\" | mysql -u root -p\"$DB_PASSWORD\"\n\necho \"GRANT ALL PRIVILEGES ON $DB_NAME.* TO '$DBUSER'@'localhost';\" | mysql -u root -p\"$DB_PASSWORD\"\necho \"FLUSH PRIVILEGES;\" | mysql -u root -p\"$DB_PASSWORD\"\n\n\n###########################################################\n# Install PHP \n###########################################################\napt-get install -y zip unzip php-mbstring php-zip php-gd php-cli php-curl php-intl php-imap php-xml php-xsl php-tokenizer php-sqlite3 php-pgsql php-opcache php-simplexml php-fpm php-bcmath php-ctype php-json php-pdo php-mysql\n\n###########################################################\n# Akaunting\n###########################################################\nmkdir -p /var/www/akaunting \\\n && curl -Lo /tmp/akaunting.zip 'https://akaunting.com/download.php?version=latest&utm_source=linode&utm_campaign=developers' \\\n && unzip /tmp/akaunting.zip -d /var/www/html \\\n && rm -f /tmp/akaunting.zip\n\ncat </var/www/html/.env\nAPP_NAME=Akaunting\nAPP_ENV=production\nAPP_LOCALE=en-GB\nAPP_INSTALLED=false\nAPP_KEY=\nAPP_DEBUG=false\nAPP_SCHEDULE_TIME=\"09:00\"\nAPP_URL=\n\nDB_CONNECTION=mysql\nDB_HOST=localhost\nDB_PORT=3306\nDB_DATABASE=${DB_NAME}\nDB_USERNAME=${DBUSER}\nDB_PASSWORD=${DBUSER_PASSWORD}\nDB_PREFIX=\n\nBROADCAST_DRIVER=log\nCACHE_DRIVER=file\nSESSION_DRIVER=file\nQUEUE_CONNECTION=sync\nLOG_CHANNEL=stack\n\nMAIL_MAILER=mail\nMAIL_HOST=localhost\nMAIL_PORT=2525\nMAIL_USERNAME=null\nMAIL_PASSWORD=null\nMAIL_ENCRYPTION=null\nMAIL_FROM_NAME=null\nMAIL_FROM_ADDRESS=null\n\nFIREWALL_ENABLED=false\nEND\n\ncd /var/www/html && php artisan key:generate\n\n# Install Akaunting\nphp /var/www/html/artisan install --db-host=\"localhost\" --db-name=\"$DB_NAME\" --db-username=\"$DBUSER\" --db-password=\"$DBUSER_PASSWORD\" --company-name=\"$COMPANY_NAME\" --company-email=\"$COMPANY_EMAIL\" --admin-email=\"$ADMIN_EMAIL\" --admin-password=\"$ADMIN_PASSWORD\"\n\n# Fix permissions\nchown -Rf www-data:www-data /var/www/html\nfind /var/www/html/ -type d -exec chmod 755 {} \\;\nfind /var/www/html/ -type f -exec chmod 644 {} \\;\n\n###########################################################\n# Configure NGINX\n###########################################################\nPHP_VERSION=$(php -r \"echo PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION;\")\ncat << END > /etc/nginx/nginx.conf\n# Generic startup file.\nuser www-data;\n\n#usually equal to number of CPUs you have. run command \"grep processor /proc/cpuinfo | wc -l\" to find it\nworker_processes auto;\nworker_cpu_affinity auto;\n\nerror_log /var/log/nginx/error.log;\npid /var/run/nginx.pid;\n\n# Keeps the logs free of messages about not being able to bind().\n#daemon off;\n\nevents {\nworker_connections 1024;\n}\n\nhttp {\n# rewrite_log on;\n\ninclude mime.types;\ndefault_type application/octet-stream;\naccess_log /var/log/nginx/access.log;\nsendfile on;\n# tcp_nopush on;\nkeepalive_timeout 64;\n# tcp_nodelay on;\n# gzip on;\n #php max upload limit cannot be larger than this \nclient_max_body_size 13m;\nindex index.php index.html index.htm;\n\n# Upstream to abstract backend connection(s) for PHP.\nupstream php {\n #this should match value of \"listen\" directive in php-fpm pool\n server unix:/run/php/php$PHP_VERSION-fpm.sock;\n server 127.0.0.1:9000;\n}\n\nserver {\n listen 80 default_server;\n\n server_name _;\n\n root /var/www/html;\n\n add_header X-Frame-Options \"SAMEORIGIN\";\n add_header X-XSS-Protection \"1; mode=block\";\n add_header X-Content-Type-Options \"nosniff\";\n\n index index.html index.htm index.php;\n\n charset utf-8;\n\n location / {\n try_files \\$uri \\$uri/ /index.php?\\$query_string;\n }\n\n # Prevent Direct Access To Protected Files\n location ~ \\.(env|log) {\n deny all;\n }\n\n # Prevent Direct Access To Protected Folders\n location ~ ^/(^app$|bootstrap|config|database|overrides|resources|routes|storage|tests|artisan) {\n deny all;\n }\n\n # Prevent Direct Access To modules/vendor Folders Except Assets\n location ~ ^/(modules|vendor)\\/(.*)\\.((?!ico|gif|jpg|jpeg|png|js\\b|css|less|sass|font|woff|woff2|eot|ttf|svg).)*$ {\n deny all;\n }\n\n error_page 404 /index.php;\n\n # Pass PHP Scripts To FastCGI Server\n location ~ \\.php$ {\n fastcgi_split_path_info ^(.+\\.php)(/.+)\\$;\n fastcgi_pass php;\n fastcgi_index index.php;\n fastcgi_param SCRIPT_FILENAME \\$document_root\\$fastcgi_script_name;\n include fastcgi_params;\n }\n\n location ~ /\\.(?!well-known).* {\n deny all;\n }\n}\n}\nEND\n\n# Remove installation screen\nrm -f /var/www/html/index.html\n\nservice nginx reload\n\n###########################################################\n# Firewall\n###########################################################\napt-get install ufw -y\nufw limit ssh\nufw allow http\nufw allow https\n\nufw --force enable\n\n###########################################################\n# Stackscript cleanup\n###########################################################\nrm /root/StackScript\nrm /root/ssinclude*\necho \"Installation complete!\"","user_defined_fields":[{"name":"company_name","label":"Company Name","example":"My Company"},{"name":"company_email","label":"Company Email","example":"my@company.com"},{"name":"admin_email","label":"Admin Email","example":"my@company.com"},{"name":"admin_password","label":"Admin Password","example":"s3cur39a55w0r0"},{"name":"db_name","label":"MySQL Database Name","example":"akaunting"},{"name":"db_password","label":"MySQL root Password","example":"s3cur39a55w0r0"},{"name":"dbuser","label":"MySQL Username","example":"akaunting"},{"name":"dbuser_password","label":"MySQL User Password","example":"s3cur39a55w0r0"}]},{"id":985374,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Ant Media Server: Enterprise Edition One-Click","description":"Ant Media Enterprise Edition One-Click","ordinal":13,"logo_url":"assets/antmediaserver.svg","images":["linode/ubuntu20.04"],"deployments_total":1423,"deployments_active":66,"is_public":true,"mine":false,"created":"2022-03-08T17:39:39","updated":"2024-01-09T13:16:50","rev_note":"","script":"#!/usr/bin/env bash\n\nset -x\n## REQUIRED IN EVERY MARKETPLACE SUBMISSION\n# Add Logging to /var/log/stackscript.log for future troubleshooting\nexec 1> >(tee -a \"/var/log/stackscript.log\") 2>&1\n# System Updates updates\napt-get -o Acquire::ForceIPv4=true update -y\n## END OF REQUIRED CODE FOR MARKETPLACE SUBMISSION\n\nZIP_FILE=\"https://antmedia.io/linode/antmedia_2.5.3.zip\"\nINSTALL_SCRIPT=\"https://raw.githubusercontent.com/ant-media/Scripts/master/install_ant-media-server.sh\"\n\nwget -q --no-check-certificate $ZIP_FILE -O /tmp/antmedia.zip && wget -q --no-check-certificate $INSTALL_SCRIPT -P /tmp/\n\nif [ $? == \"0\" ]; then\n bash /tmp/install_ant-media-server.sh -i /tmp/antmedia.zip\nelse\n logger \"There is a problem in installing the ant media server. Please send the log of this console to contact@antmedia.io\"\n exit 1\nfi","user_defined_fields":[]},{"id":804144,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Ant Media Server: Community Edition One-Click","description":"Ant Media Server One-Click","ordinal":14,"logo_url":"assets/antmediaserver.svg","images":["linode/ubuntu20.04"],"deployments_total":5786,"deployments_active":447,"is_public":true,"mine":false,"created":"2021-04-01T12:50:57","updated":"2024-01-10T18:39:12","rev_note":"","script":"#!/usr/bin/env bash \n\n## Enable logging\nset -o pipefail\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n\nZIP_FILE=\"https://github.com/ant-media/Ant-Media-Server/releases/download/ams-v2.5.3/ant-media-server-community-2.5.3.zip\"\n\n\nINSTALL_SCRIPT=\"https://raw.githubusercontent.com/ant-media/Scripts/master/install_ant-media-server.sh\"\n\nwget -q --no-check-certificate $ZIP_FILE -O /tmp/antmedia.zip && wget -q --no-check-certificate $INSTALL_SCRIPT -P /tmp/\n\nif [ $? == \"0\" ]; then\n bash /tmp/install_ant-media-server.sh -i /tmp/antmedia.zip\nelse\n logger \"There is a problem in installing the ant media server. Please send the log of this console to contact@antmedia.io\"\n exit 1\nfi","user_defined_fields":[]},{"id":1102900,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Apache Airflow One-Click","description":"Apache Airflow One-Click App","ordinal":15,"logo_url":"assets/apacheairflow.svg","images":["linode/ubuntu20.04"],"deployments_total":129,"deployments_active":4,"is_public":true,"mine":false,"created":"2022-12-20T17:32:08","updated":"2024-01-02T20:07:21","rev_note":"","script":"#!/bin/bash\n#\n# \n## Linode/SSH Security Settings\n#\n#\n#\n#\n## Domain Settings\n#\n#\n#\n## Enable logging\n\nset -x\n## REQUIRED IN EVERY MARKETPLACE SUBMISSION\n# Add Logging to /var/log/stackscript.log for future troubleshooting\nexec 1> >(tee -a \"/var/log/stackscript.log\") 2>&1\n# System Updates updates\napt-get -o Acquire::ForceIPv4=true update -y\n## END OF REQUIRED CODE FOR MARKETPLACE SUBMISSION\n\n## Import the Bash StackScript Library\nsource \n## Import the DNS/API Functions Library\nsource \n## Import the OCA Helper Functions\nsource \n## Run initial configuration tasks (DNS/SSH stuff, etc...)\nsource \n\n## Register default rDNS \nexport DEFAULT_RDNS=$(dnsdomainname -A | awk '{print $1}')\n\n#set absolute domain if any, otherwise use DEFAULT_RDNS\nif [[ $DOMAIN = \"\" ]]; then\n readonly ABS_DOMAIN=\"$DEFAULT_RDNS\"\nelif [[ $SUBDOMAIN = \"\" ]]; then\n readonly ABS_DOMAIN=\"$DOMAIN\"\nelse\n readonly ABS_DOMAIN=\"$SUBDOMAIN.$DOMAIN\"\nfi\n\ncreate_a_record $SUBDOMAIN $IP $DOMAIN\n\n# install depends\nexport DEBIAN_FRONTEND=noninteractive\nsudo apt update\n#sudo apt -y upgrade\nsudo apt install -y python3-pip\nsudo apt install -y build-essential libssl-dev libffi-dev python3-dev\nsudo apt install -y python3-venv # One of the Airflow examples requires virtual environments\n\nexport AIRFLOW_HOME=~/airflow\n\n# Install Airflow using the constraints file\nAIRFLOW_VERSION=2.4.1\nPYTHON_VERSION=\"$(python3 --version | cut -d \" \" -f 2 | cut -d \".\" -f 1-2)\"\n# For example: 3.7\nCONSTRAINT_URL=\"https://raw.githubusercontent.com/apache/airflow/constraints-${AIRFLOW_VERSION}/constraints-${PYTHON_VERSION}.txt\"\n# For example: https://raw.githubusercontent.com/apache/airflow/constraints-2.4.1/constraints-3.7.txt\npip install \"apache-airflow==${AIRFLOW_VERSION}\" --constraint \"${CONSTRAINT_URL}\"\n\n# The Standalone command will initialise the database, make a user,\n# and start all components for you.\nairflow standalone &\n\n###\n# \n# systemd unit file and per component settings go here\n# \n### \n\n\n## install nginx reverse-proxy \napt install nginx -y \n\n#configure nginx reverse proxy\nrm /etc/nginx/sites-enabled/default\ntouch /etc/nginx/sites-available/reverse-proxy.conf\ncat < /etc/nginx/sites-available/reverse-proxy.conf\nserver {\n listen 80;\n listen [::]:80;\n server_name ${DEFAULT_RDNS};\n\n access_log /var/log/nginx/reverse-access.log;\n error_log /var/log/nginx/reverse-error.log;\n\n location / {\n proxy_pass http://localhost:8080;\n proxy_set_header Host \\$host;\n proxy_set_header X-Real-IP \\$remote_addr;\n proxy_set_header X-Forward-For \\$proxy_add_x_forwarded_for;\n }\n}\nEND\nln -s /etc/nginx/sites-available/reverse-proxy.conf /etc/nginx/sites-enabled/reverse-proxy.conf\n\n#enable and start nginx\nsystemctl enable nginx\nsystemctl restart nginx \n\n## UFW rules \nufw allow http \nufw allow https \nsystemctl enable ufw\n\nsleep 60 \n\n## install SSL certs. required \npip install pyOpenSSL --upgrade\napt install python3-certbot-nginx -y \ncertbot run --non-interactive --nginx --agree-tos --redirect -d ${ABS_DOMAIN} -m ${SOA_EMAIL_ADDRESS} -w /var/www/html/\n\n## write some login details\nexport ADMIN_PASS=$(cat /root/airflow/standalone_admin_password.txt)\ncat < /etc/motd \nThe installation of Apache Airflow is now complete, and the application is running in standalone mode.\n#\nYou can log into the Airflow GUI at ${ABS_DOMAIN}\nWith the credentials: \nUsername: admin\nPassword: ${ADMIN_PASS}\n#\nStandalone mode is not recommended for production.\nEND\n\nstackscript_cleanup","user_defined_fields":[{"name":"soa_email_address","label":"Email address (for the Let's Encrypt SSL certificate)","example":"user@domain.tld"},{"name":"username","label":"The limited sudo user to be created for the Linode.","default":""},{"name":"password","label":"The password for the limited sudo user","example":"an0th3r_s3cure_p4ssw0rd","default":""},{"name":"pubkey","label":"The SSH Public Key that will be used to access the Linode","default":""},{"name":"disable_root","label":"Disable root access over SSH?","oneof":"Yes,No","default":"No"},{"name":"token_password","label":"Your Linode API token. This is needed to create your Linode's DNS records","default":""},{"name":"subdomain","label":"Subdomain","example":"The subdomain for the DNS record: www (Requires Domain)","default":""},{"name":"domain","label":"Domain","example":"The domain for the DNS record: example.com (Requires API token)","default":""}]},{"id":1160820,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Appwrite One-Click","description":"Appwrite One-Click ","ordinal":16,"logo_url":"assets/appwrite.svg","images":["linode/ubuntu22.04"],"deployments_total":199,"deployments_active":14,"is_public":true,"mine":false,"created":"2023-04-21T13:09:13","updated":"2024-01-09T18:28:28","rev_note":"","script":"#!/bin/bash\n### linode \n## Enable logging\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n\n# install docker\ncurl -fsSL https://get.docker.com -o get-docker.sh\nbash ./get-docker.sh\n\n# install haveged\nsudo apt-get install -y haveged\n\n# Install Appwrite\n# Grab latest version\nappversion=$(curl -s https://api.github.com/repos/appwrite/appwrite/releases/latest | grep -oP '\"tag_name\": \"\\K.*?(?=\")')\n\ndocker run --rm \\\n --volume /var/run/docker.sock:/var/run/docker.sock \\\n --volume \"$(pwd)\"/appwrite:/usr/src/code/appwrite:rw \\\n appwrite/appwrite:$appversion sh -c \"install --httpPort=80 --httpsPort=443 --interactive=N\"\n\necho \"Installation complete!\"","user_defined_fields":[]},{"id":401699,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Ark One-Click","description":"Ark - Latest One-Click","ordinal":17,"logo_url":"assets/Ark@1x.svg","images":["linode/debian11"],"deployments_total":1153,"deployments_active":4,"is_public":true,"mine":false,"created":"2019-03-08T21:05:54","updated":"2023-12-29T06:29:50","rev_note":"Remove SSH Pubkey UDF","script":"#!/bin/bash\n#\n#\n#\n#\n#\n#\n#\n#\n\nsource \nsource \nsource \nsource \n\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\nset -o pipefail\n\nGAMESERVER=\"arkserver\"\n\nset_hostname\napt_setup_update\n\n\n# ARK specific dependencies\ndebconf-set-selections <<< \"postfix postfix/main_mailer_type string 'No Configuration'\"\ndebconf-set-selections <<< \"postfix postfix/mailname string `hostname`\"\ndpkg --add-architecture i386\napt update\nsudo apt -q -y install mailutils postfix \\\ncurl wget file bzip2 gzip unzip bsdmainutils \\\npython util-linux ca-certificates binutils bc \\\njq tmux lib32gcc-s1 libstdc++6 libstdc++6:i386 \n\n# Install linuxGSM\nlinuxgsm_install\n\n# Install ARK\ngame_install\n\n# Setup crons and create systemd service file\nservice_config\n\n#Game Config Options\n\nsed -i s/XPMultiplier=.*/XPMultiplier=\"$XPMULTIPLIER\"/ /home/arkserver/serverfiles/ShooterGame/Saved/Config/LinuxServer/GameUserSettings.ini\nsed -i s/ServerPassword=.*/ServerPassword=\"$SERVERPASSWORD\"/ /home/arkserver/serverfiles/ShooterGame/Saved/Config/LinuxServer/GameUserSettings.ini\nsed -i s/ServerHardcore=.*/ServerHardcore=\"$SERVERPASSWORD\"/ /home/arkserver/serverfiles/ShooterGame/Saved/Config/LinuxServer/GameUserSettings.ini\nsed -i s/ServerPVE=.*/ServerPVE=\"$SERVERPVE\"/ /home/arkserver/serverfiles/ShooterGame/Saved/Config/LinuxServer/GameUserSettings.ini\nsed -i s/Message=.*/Message=\"$MOTD\"/ /home/arkserver/serverfiles/ShooterGame/Saved/Config/LinuxServer/GameUserSettings.ini\nsed -i s/SessionName=.*/SessionName=\"$SESSIONNAME\"/ /home/arkserver/serverfiles/ShooterGame/Saved/Config/LinuxServer/GameUserSettings.ini\nsed -i s/ServerAdminPassword=.*/ServerAdminPassword=\"\\\"$RCONPASSWORD\\\"\"/ /home/arkserver/serverfiles/ShooterGame/Saved/Config/LinuxServer/GameUserSettings.ini\n\n\n# Start the service and setup firewall\nufw_install\nufw allow 27015/udp\nufw allow 7777:7778/udp\nufw allow 27020/tcp\nufw enable\nfail2ban_install\nsystemctl start \"$GAMESERVER\".service\nsystemctl enable \"$GAMESERVER\".service\nstackscript_cleanup","user_defined_fields":[{"name":"rconpassword","label":"RCON password"},{"name":"sessionname","label":"Server Name","default":"Ark Server"},{"name":"motd","label":"Message of the Day","default":"Powered by Linode!"},{"name":"serverpassword","label":"Server Password","default":""},{"name":"hardcore","label":"Hardcore Mode Enabled","oneof":"True,False","default":"False"},{"name":"xpmultiplier","label":"XP Multiplier","oneof":"1,1.5,2,5,10,20","default":"2"},{"name":"serverpve","label":"Server PvE","oneof":"True,False","default":"False"}]},{"id":662118,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Azuracast One-Click","description":"AzuraCast One-Click","ordinal":18,"logo_url":"assets/azuracast.svg","images":["linode/debian10","linode/ubuntu20.04"],"deployments_total":2755,"deployments_active":198,"is_public":true,"mine":false,"created":"2020-08-12T15:50:09","updated":"2024-01-09T19:03:45","rev_note":"","script":"#!/bin/bash\n\nsource \nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n\n# Set hostname, apt configuration and update/upgrade\nset_hostname\napt_setup_update\n\n# Install GIT\napt-get update && apt-get install -q -y git\n# Cloning AzuraCast and install\nmkdir -p /var/azuracast\ncd /var/azuracast\ncurl -fsSL https://raw.githubusercontent.com/AzuraCast/AzuraCast/main/docker.sh > docker.sh\nchmod a+x docker.sh\nyes 'Y' | ./docker.sh setup-release\nyes '' | ./docker.sh install\n\n# Cleanup\nstackscript_cleanup","user_defined_fields":[]},{"id":913277,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"BeEF One-Click","description":"BeEF One-Click","ordinal":19,"logo_url":"assets/beef.svg","images":["linode/ubuntu22.04"],"deployments_total":30932,"deployments_active":1076,"is_public":true,"mine":false,"created":"2021-09-30T18:28:58","updated":"2024-01-10T21:38:51","rev_note":"","script":"#!/bin/bash\n#\n# Script to install BEEF on Linode\n# \n# \n## Linode/SSH Security Settings\n#\n#\n#\n#\n## Domain Settings\n#\n#\n#\n## Enable logging\nset -o pipefail\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n## Import the Bash StackScript Library\nsource \n## Import the DNS/API Functions Library\nsource \n## Import the OCA Helper Functions\nsource \n## Run initial configuration tasks (DNS/SSH stuff, etc...)\nsource \nbeef_config=\"/home/beef/config.yaml\"\nkey=\"privkey.pem\"\ncert=\"fullchain.pem\"\n# System Update\napt_setup_update\n# UFW\nufw allow 80\nufw allow 443\nufw allow 3000\nfunction configure_nginx {\n apt install git nginx ruby-dev -y\n # NGINX\n mkdir -p /var/www/certs/.well-known\n chown -R www-data:www-data /var/www/certs/\n cat < /etc/nginx/sites-available/$FQDN\nserver {\n listen 80;\n listen [::]:80;\n server_name $FQDN;\n root /var/www/certs;\n location / {\n try_files \\$uri \\$uri/ =404;\n }\n# allow .well-known\n location ^~ /.well-known {\n allow all;\n auth_basic off;\n alias /var/www/certs/.well-known;\n }\n}\nEOF\n ln -s /etc/nginx/sites-available/$FQDN /etc/nginx/sites-enabled/$FQDN\n unlink /etc/nginx/sites-enabled/default\n systemctl restart nginx\n}\nfunction configure_ssl {\n apt install certbot python3-certbot-nginx -y\n certbot_ssl \"$FQDN\" \"$SOA_EMAIL_ADDRESS\" 'nginx'\n}\nfunction create_beef_user {\n function create_beef {\n groupadd --system beef\n useradd -s /sbin/nologin --system -g beef beef\n }\n grep beef /etc/passwd\n if [ $? -eq 1 ];then\n create_beef\n else\n echo \"[INFO] beef already on the system. Deleting user\"\n deluser --remove-home beef\n create_beef\n fi\n}\nfunction configure_beef {\n git clone https://github.com/beefproject/beef.git /home/beef\n chown -R beef: /home/beef\n cd /home/beef\n cp /etc/letsencrypt/live/$FQDN/$key .\n cp /etc/letsencrypt/live/$FQDN/$cert .\n # get line number to replace\n get_https_enable=$(grep -n -C 10 \"key:\" $beef_config | grep -v \"#\" | grep \"https:\" -A 5 | grep \"enable:\" | awk -F \"-\" {'print $1'})\n get_https_public_enabled=$(grep -n -C 10 \"key:\" $beef_config | grep -v \"#\" | grep \"https:\" -A 5 | grep \"public_enabled:\" | awk -F \"-\" {'print $1'})\n # replacing line numebr\n sed -i \"\"$get_https_enable\"s/enable: false/enable: true/\" $beef_config\n sed -i \"\"$get_https_public_enabled\"s/public_enabled: false/public_enabled: true/\" $beef_config\n sed -i \"/key:/c\\ key: \\\"$key\\\"\" $beef_config\n sed -i \"/cert:/c\\ cert: \\\"$cert\\\"\" $beef_config\n # creds\n #sed -i \"/user:/c\\ user: \\\"beef\\\"\" $beef_config\n sed -i \"/passwd:/c\\ passwd: \\\"$BEEFPASSWORD\\\"\" $beef_config\n # install local copy of beef\n # install deps\n apt install curl git build-essential openssl libreadline6-dev zlib1g zlib1g-dev libssl-dev libyaml-dev libsqlite3-0 libsqlite3-dev sqlite3 libxml2-dev libxslt1-dev autoconf libc6-dev libncurses5-dev automake libtool bison nodejs libcurl4-openssl-dev ruby-dev -y\n su - -s /bin/bash beef\n bundle3.0 config set --local path /home/beef/.gem\n bundle3.0 install\n gem install --user-install xmlrpc\n \n}\nfunction beef_startup {\n cat < /home/beef/start_beef\n#!/bin/bash\nfunction start_beef {\n cd /home/beef\n echo no | ./beef\n}\nstart_beef\nEOF\n chown -R beef:beef /home/beef\n chmod +x /home/beef/start_beef\n}\n \nfunction beef_job {\n cat < /etc/systemd/system/beef.service\n[Unit]\nDescription=Browser Exploitation Framework\nWants=network-online.target\nAfter=network-online.target\n[Service]\nUser=beef\nGroup=beef\nExecStart=/home/beef/start_beef\n[Install]\nWantedBy=default.target\nEOF\n systemctl daemon-reload\n systemctl start beef\n systemctl enable beef\n}\nfunction ssl_renew_cron {\n cat </root/certbot-beef-renewal.sh\n#!/bin/bash\n#\n# Script to handle Certbot renewal & BeEf\n# Debug\n# set -xo pipefail\nexport BEEF_FULL=/home/beef/fullchain.pem\nexport BEEF_PRIVKEY=/home/beef/privkey.pem\nexport FULLCHAIN=/etc/letsencrypt/live/$FQDN/fullchain.pem\nexport PRIVKEY=/etc/letsencrypt/live/$FQDN/privkey.pem\ncertbot renew\ncat \\$FULLCHAIN > \\$BEEF_FULL\ncat \\$PRIVKEY > \\$BEEF_PRIVKEY\nservice beef reload\nEND\n chmod +x /root/certbot-beef-renewal.sh\n# Setup Cron\n crontab -l > cron\n echo \"* 1 * * 1 bash /root/certbot-beef-renewal.sh\" >> cron\n crontab cron\n rm cron\n}\nfunction install_complete {\n cat < /root/beef.info\n##############################\n# BEEF INSTALLATION COMPLETE #\n##############################\nEndpoint: https://$FQDN:3000/ui/panel\nCredentials can be found here:\n/home/beef/config.yaml\nHappy hunting!\nEOF\n}\nfunction main {\n create_beef_user\n configure_nginx\n configure_ssl\n configure_beef\n beef_startup\n beef_job\n ssl_renew_cron\n install_complete\n}\nmain\n# Clean up\nstackscript_cleanup\ncat /root/beef.info","user_defined_fields":[{"name":"beefpassword","label":"BEEF Password"},{"name":"soa_email_address","label":"Email address (for the Let's Encrypt SSL certificate)","example":"user@domain.tld"},{"name":"username","label":"The limited sudo user to be created for the Linode. The username cannot contain any spaces or capitol letters. For this application the username 'beef' is reserved for the application, so please choose an alternative username for this deployment.","default":""},{"name":"password","label":"The password for the limited sudo user","example":"an0th3r_s3cure_p4ssw0rd","default":""},{"name":"pubkey","label":"The SSH Public Key that will be used to access the Linode","default":""},{"name":"disable_root","label":"Disable root access over SSH?","oneof":"Yes,No","default":"No"},{"name":"token_password","label":"Your Linode API token. This is needed to create your WordPress server's DNS records","default":""},{"name":"subdomain","label":"Subdomain","example":"The subdomain for the DNS record: www (Requires Domain)","default":""},{"name":"domain","label":"Domain","example":"The domain for the DNS record: example.com (Requires API token)","default":""}]},{"id":923034,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"BitNinja One-Click","description":"BitNinja One-Click","ordinal":20,"logo_url":"assets/bitninja.svg","images":["linode/centos7","linode/debian10","linode/ubuntu20.04","linode/debian11"],"deployments_total":37,"deployments_active":0,"is_public":true,"mine":false,"created":"2021-10-18T01:03:02","updated":"2023-12-31T17:37:27","rev_note":"","script":"#!bin/bash\n\n# \n\n## Enable logging\nset -o pipefail\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n\nwget -qO- https://get.bitninja.io/install.sh | /bin/bash -s - --license_key=\"$license_key\" -y","user_defined_fields":[{"name":"license_key","label":"License Key"}]},{"id":1037036,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Budibase One-Click","description":"Budibase One Click App","ordinal":21,"logo_url":"assets/budibase.svg","images":["linode/debian11","linode/ubuntu22.04"],"deployments_total":455,"deployments_active":20,"is_public":true,"mine":false,"created":"2022-08-02T18:42:41","updated":"2024-01-10T17:24:07","rev_note":"","script":"#!/bin/bash\n#\n\nsource \nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n\n# Set hostname, configure apt and perform update/upgrade\nset_hostname\napt_setup_update\n\n# Install the dependencies & add Docker to the APT repository\napt install -y apt-transport-https ca-certificates curl software-properties-common gnupg2 pwgen ufw\ncurl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add -\nadd-apt-repository \"deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable\"\n\n# Update & install Docker-CE\napt_setup_update\napt install -y docker.io\n\n# Check to ensure Docker is running and installed correctly\nsystemctl status docker\ndocker -v\n\n# Install Docker Compose\ncurl -L https://github.com/docker/compose/releases/download/1.22.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose\nchmod +x /usr/local/bin/docker-compose\ndocker-compose --version\n\necho \"Creating passwords for /opt/budibase/.env\"\nVAR_JWT_SECRET=$(pwgen 16)\nVAR_MINIO_ACCESS_KEY=$(pwgen 16)\nVAR_MINIO_SECRET_KEY=$(pwgen 16)\nVAR_COUCH_DB_PASSWORD=$(pwgen 16)\nVAR_REDIS_PASSWORD=$(pwgen 16)\nVAR_INTERNAL_API_KEY=$(pwgen 16)\nIP=`hostname -I | awk '{print$1}'`\n\nmkdir -p /opt/budibase\ncd /opt/budibase\necho \"Fetch budibase docker compose file\"\ncurl -L https://raw.githubusercontent.com/Budibase/budibase/master/hosting/docker-compose.yaml -o /opt/budibase/docker-compose.yml\necho \"Fetch budibase .env template\"\ncurl -L https://raw.githubusercontent.com/Budibase/budibase/master/hosting/.env -o /opt/budibase/.env\necho \"Set passwords in /opt/budibase/.env\"\nsed -i \"s/JWT_SECRET=testsecret/JWT_SECRET=$VAR_JWT_SECRET/\" /opt/budibase/.env\nsed -i \"s/MINIO_ACCESS_KEY=budibase/MINIO_ACCESS_KEY=$VAR_MINIO_ACCESS_KEY/\" /opt/budibase/.env\nsed -i \"s/MINIO_SECRET_KEY=budibase/MINIO_SECRET_KEY=$VAR_MINIO_SECRET_KEY/\" /opt/budibase/.env\nsed -i \"s/COUCH_DB_PASSWORD=budibase/COUCH_DB_PASSWORD=$VAR_COUCH_DB_PASSWORD/\" /opt/budibase/.env\nsed -i \"s/REDIS_PASSWORD=budibase/REDIS_PASSWORD=$VAR_REDIS_PASSWORD/\" /opt/budibase/.env\nsed -i \"s/INTERNAL_API_KEY=budibase/INTERNAL_API_KEY=$VAR_INTERNAL_API_KEY/\" /opt/budibase/.env\nsed -i \"s/MAIN_PORT=10000/MAIN_PORT=$BBPORT/\" /opt/budibase/.env\ndocker-compose up -d\n\ncat </etc/profile.d/budibase_welcome.sh\n#!/bin/sh\n#\nIP=$(hostname -I | awk '{print$1}')\necho \"\n********************************************************************************\nWelcome to Budibase!\nTo help keep this server secure, the UFW firewall is enabled.\nAll ports are BLOCKED except 22 (SSH) and the Web UI port $BBPORT.\n********************************************************************************\n # Budibase UI: http://$IP:$BBPORT/\n # Website: https://budibase.com\n # Documentation: https://docs.budibase.com\n # Github: https://github.com/Budibase/budibase\n # Community Support: https://github.com/Budibase/budibase/discussions\n # Restart Budibase: cd /opt/budibase; docker-compose down; docker-compose up -d\n # Budibase config: /etc/budibase/.env\n\"\nEND\nchmod +x /etc/profile.d/budibase_welcome.sh\n# Enable UFW and add some rules to it\nufw enable\nufw limit ssh/tcp comment 'Rate limit the SSH port'\nufw allow $BBPORT/tcp comment \"TCP Listen port for Budibase\"\nufw --force enable\n\n# Cleanup\nstackscript_cleanup","user_defined_fields":[{"name":"BBPORT","label":"Budibase Port","example":"Default: 80","default":"80"}]},{"id":869155,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Chevereto One-Click","description":"Chevereto One-Click","ordinal":22,"logo_url":"assets/chevereto.svg","images":["linode/ubuntu20.04"],"deployments_total":241,"deployments_active":6,"is_public":true,"mine":false,"created":"2021-07-20T19:07:56","updated":"2024-01-10T18:25:18","rev_note":"","script":"#!/usr/bin/env bash\n# https://github.com/chevereto/linode-marketplace\n\nset -e\n\nCHEVERETO_INSTALLER_TAG=\"3.1.0\"\nWORKING_DIR=\"/var/www/html\"\n\n## REQUIRED IN EVERY MARKETPLACE SUBMISSION\n# Add Logging to /var/log/stackscript.log for future troubleshooting\nexec 1> >(tee -a \"/var/log/stackscript.log\") 2>&1\n\n## 03-force-ssh-logout.sh\ncat >>/etc/ssh/sshd_config </dev/null\napt install -y apache2 libapache2-mod-php\napt install -y mysql-server\napt install -y php\napt install -y php-{common,cli,curl,fileinfo,gd,imagick,intl,json,mbstring,mysql,opcache,pdo,pdo-mysql,xml,xmlrpc,zip}\napt install -y python3-certbot-apache software-properties-common unzip\n\n# 01-fs.sh\ncat >/etc/apache2/sites-available/000-default.conf <\n \n Options Indexes FollowSymLinks\n AllowOverride All\n Require all granted\n \n ServerAdmin webmaster@localhost\n DocumentRoot /var/www/html\n ErrorLog \\${APACHE_LOG_DIR}/error.log\n CustomLog \\${APACHE_LOG_DIR}/access.log combined\n\nEOM\n\ncat >/etc/update-motd.d/99-one-click < certbot --apache -d example.com -d www.example.com\nIMPORTANT:\n * After connecting to the server for the first time, immediately install\n Chevereto at http://\\$myip/installer.php\n * Secure your database by running:\n > mysql_secure_installation\n * Setup email delivery at http://\\$myip/dashboard/settings/email\nFor help and more information visit https://chevereto.com\n********************************************************************************\nTo delete this message of the day: rm -rf \\$(readlink -f \\${0})\nEOF\nEOM\nchmod +x /etc/update-motd.d/99-one-click\n\ncat >/etc/cron.d/chevereto </etc/php/7.4/apache2/conf.d/chevereto.ini <>/var/log/per-instance.log\n\nMYSQL_ROOT_PASS=$(openssl rand -hex 16)\nDEBIAN_SYS_MAINT_MYSQL_PASS=$(openssl rand -hex 16)\n\nCHEVERETO_DB_HOST=localhost\nCHEVERETO_DB_PORT=3306\nCHEVERETO_DB_NAME=chevereto\nCHEVERETO_DB_USER=chevereto\nCHEVERETO_DB_PASS=$(openssl rand -hex 16)\n\ncat >/root/.mysql_password <>/etc/apache2/envvars </etc/mysql/debian.cnf <>/var/log/per-instance.log\n\necho \"[OK] Chevereto Installer $CHEVERETO_INSTALLER_TAG provisioned!\"","user_defined_fields":[]},{"id":869158,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"ClusterControl One-Click","description":"ClusterControl One-Click","ordinal":23,"logo_url":"assets/clustercontrol.svg","images":["linode/ubuntu20.04"],"deployments_total":177,"deployments_active":2,"is_public":true,"mine":false,"created":"2021-07-20T19:13:44","updated":"2023-12-29T17:52:35","rev_note":"","script":"#!/usr/bin/env bash\n\n### UDF Variables\n\n## Severalnines settings\n#\n#\n\n## Domain settings\n#\n#\n#\n#\n\n## Let's Encrypt SSL\n#\n\n### Logging and other debugging helpers\n\n# Enable logging for the StackScript\nset -o pipefail\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n\n# Source the Linode Bash StackScript, API, and LinuxGSM Helper libraries\nsource \nsource \n\n# Source and run the New Linode Setup script for DNS/SSH configuration\nsource \n\n# System Update\nsystem_update\n\nworkdir=/tmp\nIP=`hostname -I | awk '{print$1}'`\n# if command -v dig &>/dev/null; then\n# echo -e \"\\nDetermining network interfaces.\" \n# ext_ip=$(dig +short myip.opendns.com @resolver1.opendns.com 2>/dev/null)\n# [[ ! -z $ext_ip ]] && IP=${ext_ip}\n# fi\nlog_progress() {\n\n echo \"$1\" >> /root/cc_install.log\n}\n\ninstall_cc() {\n export HOME=/root\n export USER=root\n wget --no-check-certificate https://severalnines.com/downloads/cmon/install-cc\n chmod +x install-cc\n echo \"mysql cmon password = $CMONUSER_PASSWORD\" >> /root/.cc_passwords\n echo \"mysql root password = $DBROOT_PASSWORD\" >> /root/.cc_passwords\n SEND_DIAGNOSTICS=0 S9S_CMON_PASSWORD=$CMONUSER_PASSWORD S9S_ROOT_PASSWORD=$DBROOT_PASSWORD INNODB_BUFFER_POOL_SIZE=256 ./install-cc\n}\n\nfirstboot() {\n hostnamectl set-hostname clustercontrol\n\n ssh-keygen -b 2048 -t rsa -f /root/.ssh/id_rsa -q -N \"\"\n ssh-keygen -y -f /root/.ssh/id_rsa > /root/.ssh/id_rsa.pub\n SSH_KEY=$(cat /root/.ssh/id_rsa.pub)\n\n cat < /etc/update-motd.d/99-cc-motd \n#!/bin/sh\necho \"###\"\necho \"\"\necho \"Welcome to Severalnines Database Monitoring and Management Application - ClusterControl\"\necho \"Open your web browser to http://${IP}/clustercontrol to access ClusterControl's web application\"\necho \"\"\necho \"The public SSH key (root) is:\"\necho \"$SSH_KEY\"\necho \"\"\necho \"###\"\nEND\n\n chmod +x /etc/update-motd.d/99-cc-motd\n}\n\nenable_fw() {\n ufw default deny incoming\n ufw default allow outgoing\n ufw allow ssh\n ufw allow http\n ufw allow https\n ufw allow 9999\n ufw allow 9501\n}\n\ncleanup() {\n rm -rf /tmp/* /var/tmp/* /root/scripts\n history -c\n cat /dev/null > /root/.bash_history\n unset HISTFILE\n\n apt-get -y autoremove\n apt-get -y autoclean\n\n cat /dev/null > /var/log/lastlog; cat /dev/null > /var/log/wtmp; cat /dev/null > /var/log/auth.log\n\n ufw enable\n ufw status\n\n touch /.cc-provisioned\n}\n\nlog_progress \"** Installing ClusterControl, this could take several minutes. Please wait ...\"\ninstall_cc\nlog_progress \"** Setting motd ...\"\nfirstboot\nlog_progress \"** Enabling firewall ...\"\nenable_fw\nif [[ \"$SSL\" == \"Yes\" ]]; then\n log_progress \"** Enabling Let's Encrypt SSL ...\"\n python --version | grep -q 3.\n [[ $? -eq 0 ]] && PYTHON3=1\n if [[ -n $PYTHON3 ]]; then\n apt install -y certbot python3-certbot-apache\n else\n apt install -y certbot python-certbot-apache\n fi\n\n certbot_ssl \"$FQDN\" \"$SOA_EMAIL_ADDRESS\" 'apache'\nfi\ncleanup\n\n# Clean up\nlog_progress \"** Stackscript cleanup please wait ...\"\nstackscript_cleanup\n\nlog_progress \"** Installation successful...\"\n/etc/update-motd.d/99-cc-motd | tee -a /root/cc_install.log\n\nsystemctl restart sshd","user_defined_fields":[{"name":"dbroot_password","label":"MySQL Root Password"},{"name":"cmonuser_password","label":"CMON user password"},{"name":"token_password","label":"Your Linode API token. This is required in order to create DNS records.","default":""},{"name":"subdomain","label":"The subdomain for the Linode's DNS record (Requires API token)","default":""},{"name":"domain","label":"The domain for the Linode's DNS record (Requires API token)","default":""},{"name":"soa_email_address","label":"E-Mail Address","example":"Your email address"},{"name":"ssl","label":"Would you like to use a free Let's Encrypt SSL certificate? (Uses the Linode's default rDNS if no domain is specified above)","oneof":"Yes,No","default":"Yes"}]},{"id":401700,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"CS:GO One-Click","description":"CS:GO - Latest One-Click","ordinal":24,"logo_url":"assets/CSGO2.svg","images":["linode/debian11","linode/ubuntu22.04"],"deployments_total":2142,"deployments_active":6,"is_public":true,"mine":false,"created":"2019-03-08T21:06:26","updated":"2023-11-02T20:39:58","rev_note":"Remove SSH Pubkey UDF","script":"#!/bin/bash\n#\n\n#\n#\n#\n#\n#\n#\n#\n#\n#\n#\n\nsource \nsource \nsource \nsource \n\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\nset -o pipefail\n\nGAMESERVER=\"csgoserver\"\n\n### UDF to config\n\n#Autoteambalance\nif [[ \"$AUTOTEAMBALANCE\" = \"Enabled\" ]]; then\n AUTOTEAMBALANCE=1\nelif [[ \"$AUTOTEAMBALANCE\" = \"Disabled\" ]]; then\n AUTOTEAMBALANCE=0\nfi\n\n#Buyanywhere\nif [[ \"$BUYANYWHERE\" = \"Enabled\" ]]; then\n BUYANYWHERE=1\nelif [[ \"$BUYANYWHERE\" = \"Disabled\" ]]; then\n BUYANYWHERE=0\nelif [[ \"$BUYANYWHERE\" = \"Terrorists Only\" ]]; then\n BUYANYWHERE=2\nelif [[ \"$BUYANYWHERE\" = \"Counter-Terrorists Only\" ]]; then\n BUYANYWHERE=3\nfi\n\n#friendlyfire\n\nif [[ \"$FRIENDLYFIRE\" = \"Enabled\" ]]; then\n FRIENDLYFIRE=1\nelif [[ \"$FRIENDLYFIRE\" = \"Disabled\" ]]; then\n FRIENDLYFIRE=0\nfi\n\nset_hostname\napt_setup_update\n\n\n# CSGO specific dependencies\ndebconf-set-selections <<< \"postfix postfix/main_mailer_type string 'No Configuration'\"\ndebconf-set-selections <<< \"postfix postfix/mailname string `hostname`\"\ndpkg --add-architecture i386\napt update\nsudo apt -q -y install mailutils postfix \\\ncurl wget file bzip2 gzip unzip bsdmainutils \\\npython util-linux ca-certificates binutils bc \\\njq tmux lib32gcc1 libstdc++6 libstdc++6:i386\n\n# Install linuxGSM\nlinuxgsm_install\n\n# Install CSGO\ngame_install\n\n# Setup crons and create systemd service file\nservice_config\n\n#Game Config Options\n\n> /home/csgoserver/serverfiles/csgo/cfg/csgoserver.cfg\n\ncat <> /home/csgoserver/serverfiles/csgo/cfg/csgoserver.cfg\nsv_contact \"\"\nsv_lan 0\nlog on\nsv_logbans 1\nsv_logecho 1\nsv_logfile 1\nsv_log_onefile 0\nsv_hibernate_when_empty 1\nsv_hibernate_ms 5\nhost_name_store 1\nhost_info_show 1\nhost_players_show 2\nexec banned_user.cfg\nexec banned_ip.cfg\nwriteid\nwriteip\nEND\n\necho \"mp_autoteambalance $AUTOTEAMBALANCE\" >> /home/csgoserver/serverfiles/csgo/cfg/csgoserver.cfg\necho \"hostname $SERVERNAME\" >> /home/csgoserver/serverfiles/csgo/cfg/csgoserver.cfg\necho \"mp_roundtime $ROUNDTIME\" >> /home/csgoserver/serverfiles/csgo/cfg/csgoserver.cfg\necho \"rcon_password \\\"$RCONPASSWORD\\\"\" >> /home/csgoserver/serverfiles/csgo/cfg/csgoserver.cfg\necho \"sv_password \\\"$SVPASSWORD\\\"\" >> /home/csgoserver/serverfiles/csgo/cfg/csgoserver.cfg\nsed -i s/mp_buy_anywhere.*/mp_buy_anywhere\\ \"$BUYANYWHERE\"/ /home/csgoserver/serverfiles/csgo/cfg/gamemode_casual.cfg\nsed -i s/mp_maxrounds.*/mp_maxrounds\\ \"$MAXROUNDS\"/ /home/csgoserver/serverfiles/csgo/cfg/gamemode_casual.cfg\nsed -i s/mp_friendlyfire.*/mp_friendlyfire\\ \"$FRIENDLYFIRE\"/ /home/csgoserver/serverfiles/csgo/cfg/gamemode_casual.cfg\necho \"$MOTD\" > /home/csgoserver/serverfiles/csgo/motd.txt\n\n\nif [[ \"$FRIENDLYFIRE\" = \"1\" ]]; then\nsed -i s/ff_damage_reduction_bullets.*/ff_damage_reduction_bullets\\ 0\\.85/ /home/csgoserver/serverfiles/csgo/cfg/gamemode_casual.cfg\nsed -i s/ff_damage_reduction_gernade.*/ff_damage_reduction_gernade\\ 0\\.33/ /home/csgoserver/serverfiles/csgo/cfg/gamemode_casual.cfg\nsed -i s/ff_damage_reduction_gernade_self.*/ff_damage_reduction_gernade_self\\ 0\\.4/ /home/csgoserver/serverfiles/csgo/cfg/gamemode_casual.cfg\nsed -i s/ff_damage_reduction_other.*/ff_damage_reduction_other\\ 1/ /home/csgoserver/serverfiles/csgo/cfg/gamemode_casual.cfg\necho \"sv_kick_ban_duration 0\" >> /home/csgoserver/serverfiles/csgo/cfg/csgoserver.cfg\necho \"mp_disable_autokick 0\" >> /home/csgoserver/serverfiles/csgo/cfg/csgoserver.cfg\nfi\n\n# Start the service and setup firewall\nufw_install\nufw allow 27015\nufw allow 27020/udp\nufw allow 27005/udp\nufw enable\nfail2ban_install\nsystemctl start \"$GAMESERVER\".service\nsystemctl enable \"$GAMESERVER\".service\nstackscript_cleanup","user_defined_fields":[{"name":"gslt","label":"Game Server Login Token","example":"Steam gameserver token. Needed to list as public server."},{"name":"motd","label":"Message of the Day","default":"Powered by Linode!"},{"name":"servername","label":"Server Name","default":"Linode CS:GO Server"},{"name":"rconpassword","label":"RCON password"},{"name":"svpassword","label":"CSGO server password","default":""},{"name":"autoteambalance","label":"Team Balance Enabled","oneof":"Enabled,Disabled","default":"Enabled"},{"name":"roundtime","label":"Round Time Limit","oneof":"5,10,15,20,60","default":"5"},{"name":"maxrounds","label":"Maximum Rounds","oneof":"1,5,10,15,20","default":"10"},{"name":"buyanywhere","label":"Buy Anywhere ","oneof":"Disabled,Enabled,Counter-Terrorists Only, Terrorists Only","default":"Disabled"},{"name":"friendlyfire","label":"Friendly Fire Enabled","oneof":"Enabled,Disabled","default":"Disabled"}]},{"id":688891,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Discourse One-Click","description":"Discourse One-Click","ordinal":25,"logo_url":"assets/discourse.svg","images":["linode/ubuntu20.04"],"deployments_total":1226,"deployments_active":60,"is_public":true,"mine":false,"created":"2020-11-17T20:55:26","updated":"2024-01-07T15:42:18","rev_note":"","script":"#!/bin/bash\n\n## Discourse Settings\n\n#\n#\n#\n#\n#\n#\n#\n\n## Linode/SSH Security Settings\n#\n#\n#\n#\n\n# Source the Bash StackScript Library and the API functions for DNS\nsource \nsource \nsource \n\n# Source and run the New Linode Setup script for DNS/SSH configuration\n# This also sets some useful variables, like $IP and $FQDN\nsource \n\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\nset -xo pipefail\n\n#Install dependencies needed for Discourse\napt install git apt-transport-https ca-certificates curl software-properties-common net-tools -y\n\n#Clone Discourse Docker repo for install and management\ngit clone https://github.com/discourse/discourse_docker.git /var/discourse\n#UFW Firewall Rules\nufw allow http\nufw allow https\nufw allow 25\nufw allow 465\nufw allow 587\nufw enable <\n#\n#\n\nsource \nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n\n# Set hostname, configure apt and perform update/upgrade\nset_hostname\napt_setup_update\n\n# Install Python & Django\napt-get install -y python3 python3-pip\npip3 install Django\n\n# Create & Setup Django APP\nmkdir /var/www/\ncd /var/www/\ndjango-admin startproject DjangoApp\ncd DjangoApp\npython3 manage.py migrate\necho \"from django.contrib.auth.models import User; User.objects.create_superuser('$DJANGOUSER', '$DJANGOUSEREMAIL', '$DJANGOUSERPASSWORD')\" | python3 manage.py shell\nsed -i \"s/ALLOWED_HOSTS = \\[\\]/ALLOWED_HOSTS = \\['$IP'\\]/g\" DjangoApp/settings.py\npython3 manage.py runserver 0.0.0.0:8000 &\n\n# Start Django app on reboot\ncrontab -l | { cat; echo \"@reboot cd /var/www/DjangoApp && python3 manage.py runserver 0.0.0.0:8000 &\"; } | crontab -\n\n# Cleanup\nstackscript_cleanup","user_defined_fields":[{"name":"djangouser","label":"Django USER","example":"user1"},{"name":"djangouserpassword","label":"Django Password","example":"s3cure_p4ssw0rd"},{"name":"djangouseremail","label":"Django USER email","example":"user@email.tld"}]},{"id":607433,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Docker One-Click","description":"Docker One Click App","ordinal":27,"logo_url":"assets/docker.svg","images":["linode/ubuntu22.04"],"deployments_total":36549,"deployments_active":1783,"is_public":true,"mine":false,"created":"2019-10-31T20:14:04","updated":"2024-01-10T21:06:47","rev_note":"","script":"#!/bin/bash\nset -e\ntrap \"cleanup $? $LINENO\" EXIT\n\n## Docker Settings\n\n## Linode/SSH Security Settings\n#\n#\n#\n#\n\n## Domain Settings\n#\n#\n#\n#\n\n# git repo\nexport GIT_REPO=\"https://github.com/akamai-compute-marketplace/marketplace-apps.git\"\nexport WORK_DIR=\"/tmp/marketplace-apps\" \nexport MARKETPLACE_APP=\"apps/linode-marketplace-docker\"\n\n# enable logging\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n\nfunction cleanup {\n if [ -d \"${WORK_DIR}\" ]; then\n rm -rf ${WORK_DIR}\n fi\n\n}\n\nfunction udf {\n local group_vars=\"${WORK_DIR}/${MARKETPLACE_APP}/group_vars/linode/vars\"\n if [[ -n ${SOA_EMAIL_ADDRESS} ]]; then\n echo \"soa_email_address: ${SOA_EMAIL_ADDRESS}\" >> ${group_vars};\n else echo \"No email entered\";\n fi\n\n if [[ -n ${USER_NAME} ]]; then\n echo \"username: ${USER_NAME}\" >> ${group_vars};\n else echo \"No username entered\";\n fi\n\n if [[ -n ${PASSWORD} ]]; then\n echo \"password: ${PASSWORD}\" >> ${group_vars};\n else echo \"No password entered\";\n fi\n\n if [[ -n ${PUBKEY} ]]; then\n echo \"pubkey: ${PUBKEY}\" >> ${group_vars};\n else echo \"No pubkey entered\";\n fi\n\n if [ \"$DISABLE_ROOT\" = \"Yes\" ]; then\n echo \"disable_root: yes\" >> ${group_vars};\n else echo \"Leaving root login enabled\";\n fi\n\n if [[ -n ${TOKEN_PASSWORD} ]]; then\n echo \"token_password: ${TOKEN_PASSWORD}\" >> ${group_vars};\n else echo \"No API token entered\";\n fi\n\n if [[ -n ${DOMAIN} ]]; then\n echo \"domain: ${DOMAIN}\" >> ${group_vars};\n #else echo \"No domain entered\";\n else echo \"default_dns: $(hostname -I | awk '{print $1}'| tr '.' '-' | awk {'print $1 \".ip.linodeusercontent.com\"'})\" >> ${group_vars};\n fi\n\n if [[ -n ${SUBDOMAIN} ]]; then\n echo \"subdomain: ${SUBDOMAIN}\" >> ${group_vars};\n else echo \"subdomain: www\" >> ${group_vars};\n fi\n}\n\nfunction run {\n # install dependancies\n apt-get update\n apt-get install -y git python3 python3-pip\n\n # clone repo and set up ansible environment\n git -C /tmp clone ${GIT_REPO}\n # for a single testing branch\n # git -C /tmp clone -b ${BRANCH} ${GIT_REPO}\n\n # venv\n cd ${WORK_DIR}/${MARKETPLACE_APP}\n pip3 install virtualenv\n python3 -m virtualenv env\n source env/bin/activate\n pip install pip --upgrade\n pip install -r requirements.txt\n ansible-galaxy install -r collections.yml\n\n # populate group_vars\n udf\n # run playbooks\n for playbook in provision.yml site.yml; do ansible-playbook -v $playbook; done\n}\n\nfunction installation_complete {\n echo \"Installation Complete\"\n}\n# main\nrun && installation_complete\ncleanup","user_defined_fields":[{"name":"user_name","label":"The limited sudo user to be created for the Linode","default":""},{"name":"password","label":"The password for the limited sudo user","example":"an0th3r_s3cure_p4ssw0rd","default":""},{"name":"disable_root","label":"Disable root access over SSH?","oneof":"Yes,No","default":"No"},{"name":"pubkey","label":"The SSH Public Key that will be used to access the Linode (Recommended)","default":""},{"name":"token_password","label":"Your Linode API token. This is needed to create your Linode's DNS records","default":""},{"name":"subdomain","label":"Subdomain","example":"The subdomain for the DNS record. `www` will be entered if no subdomain is supplied (Requires Domain)","default":""},{"name":"domain","label":"Domain","example":"The domain for the DNS record: example.com (Requires API token)","default":""},{"name":"soa_email_address","label":"SOA Email","example":"user@domain.tld","default":""}]},{"id":401698,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Drupal One-Click","description":"Drupal One-Click","ordinal":28,"logo_url":"assets/Drupal.svg","images":["linode/ubuntu22.04"],"deployments_total":1870,"deployments_active":80,"is_public":true,"mine":false,"created":"2019-03-08T21:04:47","updated":"2024-01-04T23:36:15","rev_note":"","script":"#!/usr/bin/env bash\n## Drupal Settings\n# \n# \n# \n\n## Linode/SSH Security Settings\n#\n#\n#\n#\n\n## Domain Settings\n#\n#\n#\n\n## Enable logging\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\nset -o pipefail\n\n## Import the Bash StackScript Library\nsource \n## Import the DNS/API Functions Library\nsource \n## Import the OCA Helper Functions\nsource \n## Run initial configuration tasks (DNS/SSH stuff, etc...)\nsource \n\n# Set hostname, apt configuration and update/upgrade\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n\n# Install/configure UFW\nufw allow http\nufw allow https\n\n# Install/configure MySQL\napt-get install mariadb-server -y\nsystemctl start mariadb\nsystemctl enable mariadb\nmysql_root_preinstall\nrun_mysql_secure_installation\nmysql -uroot -p\"$DBROOT_PASSWORD\" -e \"CREATE DATABASE drupaldb\"\nmysql -uroot -p\"$DBROOT_PASSWORD\" -e \"GRANT ALL ON drupaldb.* TO 'drupal'@'localhost' IDENTIFIED BY '$DB_PASSWORD'\";\nmysql -uroot -p\"$DBROOT_PASSWORD\" -e \"FLUSH PRIVILEGES\";\n\n# Install & Configure Apache\napt-get install -y apache2\ntouch /var/log/apache2/drupal-error_log /var/log/apache2/drupal-access_log\ncp /etc/apache2/sites-available/000-default.conf /etc/apache2/sites-available/drupal.conf\ncat < /etc/apache2/sites-available/drupal.conf\n\n DocumentRoot /var/www/drupal\n ServerName $FQDN\n ServerAlias www.$FQDN\n \n Options FollowSymLinks\n AllowOverride All\n Order allow,deny\n allow from all\n RewriteEngine on\n RewriteBase /\n RewriteCond %{REQUEST_FILENAME} !-f\n RewriteCond %{REQUEST_FILENAME} !-d\n RewriteCond %{REQUEST_URI} !=/favicon.ico\n RewriteRule ^ index.php [L]\n\n ErrorLog /var/log/apache2/drupal-error_log\n CustomLog /var/log/apache2/drupal-access_log common\n\nEND\na2enmod rewrite\na2dissite 000-default.conf\na2ensite drupal.conf\nsed -ie \"s/KeepAlive Off/KeepAlive On/g\" /etc/apache2/apache2.conf\nsystemctl restart apache2\nsystemctl enable apache2\n\n# Install PHP 8.1\napt-get install php libapache2-mod-php php-mysql php-curl php-cgi php-gd php-mbstring php-xml php-xmlrpc -y\nPHP_VERSION=$(php -r \"echo PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION;\")\ncat < /etc/php/$PHP_VERSION/apache2/php.ini\nerror_reporting = E_COMPILE_ERROR|E_RECOVERABLE_ERROR|E_ERROR|E_CORE_ERROR\nerror_log = /var/log/php/error.log\nmax_input_time = 30\nEND\nmkdir /var/log/php\nchown www-data /var/log/php\n\n# Install Drupal\nrm -r /var/www/html\ncd ~; wget -4 https://www.drupal.org/download-latest/tar.gz\ntar -xf tar.gz -C /var/www/ && mv /var/www/drupal* /var/www/drupal\nrm tar.gz\nmkdir /var/www/drupal/sites/default/files\nchmod a+w /var/www/drupal/sites/default/files\ncp /var/www/drupal/sites/default/default.settings.php /var/www/drupal/sites/default/settings.php\nchmod a+w /var/www/drupal/sites/default/settings.php\ncat <> /var/www/drupal/sites/default/settings.php\n\\$settings['trusted_host_patterns'] = [\n '^$FQDN\\$',\n];\nEND\n\n# Cleanup\nsystemctl restart apache2\nsystemctl restart mysql\n\n# SSL\napt install certbot python3-certbot-apache -y\ncertbot_ssl \"$FQDN\" \"$SOA_EMAIL_ADDRESS\" 'apache'\n\nstackscript_cleanup","user_defined_fields":[{"name":"soa_email_address","label":"E-Mail Address","example":"Your email address"},{"name":"dbroot_password","label":"MySQL root Password","example":"an0th3r_s3cure_p4ssw0rd"},{"name":"db_password","label":"Database Password","example":"an0th3r_s3cure_p4ssw0rd"},{"name":"username","label":"The limited sudo user to be created for the Linode","default":""},{"name":"password","label":"The password for the limited sudo user","example":"an0th3r_s3cure_p4ssw0rd","default":""},{"name":"pubkey","label":"The SSH Public Key that will be used to access the Linode","default":""},{"name":"disable_root","label":"Disable root access over SSH?","oneof":"Yes,No","default":"No"},{"name":"token_password","label":"Your Linode API token. This is needed to create your Drupal server's DNS records","default":""},{"name":"subdomain","label":"Subdomain","example":"The subdomain for the DNS record: www (Requires Domain)","default":""},{"name":"domain","label":"Domain","example":"The domain for the DNS record: example.com (Requires API token)","default":""}]},{"id":1008125,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Easypanel One-Click","description":"Easypanel One-Click","ordinal":29,"logo_url":"assets/easypanel.svg","images":["linode/ubuntu22.04"],"deployments_total":1474,"deployments_active":84,"is_public":true,"mine":false,"created":"2022-05-18T16:43:00","updated":"2024-01-09T21:07:04","rev_note":"","script":"#!/bin/bash\n\n# Add Logging to /var/log/stackscript.log for future troubleshooting\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n\n# install docker\ncurl -fsSL https://get.docker.com -o get-docker.sh\nsh get-docker.sh\n\n# setup easypanel\ndocker run --rm \\\n -v /etc/easypanel:/etc/easypanel \\\n -v /var/run/docker.sock:/var/run/docker.sock:ro \\\n easypanel/easypanel setup","user_defined_fields":[]},{"id":691620,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"FileCloud One-Click","description":"FileCloud One-Click","ordinal":30,"logo_url":"assets/filecloud.svg","images":["linode/ubuntu20.04"],"deployments_total":824,"deployments_active":13,"is_public":true,"mine":false,"created":"2020-11-30T21:16:19","updated":"2024-01-10T19:05:17","rev_note":"","script":"#!/bin/bash \n\n## Domain Settings\n#\n#\n#\n#\n#\n\n## Linode/SSH Security Settings\n#\n#\n#\n#\n\n# Source and run the New Linode Setup script for DNS configuration\n# This also sets some useful variables, like $IP and $FQDN\n\nsource \n\n# Source the Bash StackScript Library and the API functions for DNS\nsource \nsource \nsource \n\n# Add Logging to /var/log/stackscript.log for future troubleshooting\nset pipefail -o\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n\n# Allow traffic on ports 80 and 443\nufw allow 80\nufw allow 443\n\n# Installing Filecloud and Prequisites\nwget -qO - https://repo.filecloudlabs.com/static/pgp/filecloud.asc | sudo apt-key add -\nwget -qO - https://www.mongodb.org/static/pgp/server-4.4.asc | sudo apt-key add -\necho \"deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/4.4 multiverse\" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.4.list\necho \"deb [ arch=amd64 ] https://repo.filecloudlabs.com/apt/ubuntu focal/filecloud/22.1 main\" | sudo tee /etc/apt/sources.list.d/filecloud.list\napt-get update -y\napt-get install apache2 mongodb-org -y\napt install -y --no-install-recommends php8.1*\nACCEPT_EULA=Y apt-get install filecloud -y\n\nif [[ \"$SSL\" == \"Yes\" ]]; then\n certbot_ssl \"$FQDN\" \"$SOA_EMAIL_ADDRESS\" 'apache'\nfi\n\n# Cleanup\nstackscript_cleanup","user_defined_fields":[{"name":"token_password","label":"Your Linode API token. This is required in order to create DNS records.","default":""},{"name":"subdomain","label":"The subdomain for the Linode's DNS record (Requires API token)","default":""},{"name":"domain","label":"The domain for the Linode's DNS record (Requires API token)","default":""},{"name":"ssl","label":"Would you like to use a free CertBot SSL certificate?","oneof":"Yes,No","default":"No"},{"name":"soa_email_address","label":"Email Address for Lets' Encrypt Certificate","default":""},{"name":"username","label":"The limited sudo user to be created for the Linode","default":""},{"name":"password","label":"The password for the limited sudo user","example":"an0th3r_s3cure_p4ssw0rd","default":""},{"name":"pubkey","label":"The SSH Public Key that will be used to access the Linode","default":""},{"name":"disable_root","label":"Disable root access over SSH?","oneof":"Yes,No","default":"No"}]},{"id":609392,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Flask One-Click","description":"Flask One-Click","ordinal":31,"logo_url":"assets/flask.svg","images":["linode/debian10"],"deployments_total":2196,"deployments_active":135,"is_public":true,"mine":false,"created":"2019-11-07T06:24:17","updated":"2024-01-09T14:09:13","rev_note":"Initial import","script":"#!/bin/bash\n\n## Enable logging\nexec > /var/log/stackscript.log 2>&1\n## Import the Bash StackScript Library\nsource \n## Import the DNS/API Functions Library\nsource \n## Import the OCA Helper Functions\nsource \n## Run initial configuration tasks (DNS/SSH stuff, etc...)\nsource \n\nset -o pipefail\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n\n# Set hostname, configure apt and perform update/upgrade\nset_hostname\napt_setup_update\nufw_install\nufw allow http\n\n# Install Prereq's & Flask APP\napt install -y git\ncd /home\ngit clone https://github.com/abalarin/Flask-on-Linode.git flask_app_project\n\n# Install & configure Nginx\napt install -y nginx\ncat < /etc/nginx/sites-enabled/flask_app\nserver {\n listen 80;\n server_name $IP;\n location / {\n proxy_pass http://127.0.0.1:8000;\n proxy_set_header Host \\$host;\n proxy_set_header X-Forwarded-For \\$proxy_add_x_forwarded_for;\n }\n}\nEND\n\nunlink /etc/nginx/sites-enabled/default\nnginx -s reload\n\n# Install python & Packages\napt install -y python3 python3-pip\ncd /home/flask_app_project\npip3 install -r flask_app/requirements.txt\n\n# Configure Flask\ncat < /etc/config.json\n{\n \"SECRET_KEY\": \"$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)\",\n \"SQLALCHEMY_DATABASE_URI\": \"sqlite:///site.db\"\n}\nEND\n\ncat < /home/flask_app_project/flask_app/__init__.py\nfrom flask import Flask\nfrom flask_sqlalchemy import SQLAlchemy\nfrom flask_login import LoginManager\nimport json\nimport urllib3\napp = Flask(__name__)\nwith open('/etc/config.json') as config_file:\n config = json.load(config_file)\napp.config['SECRET_KEY'] = config.get('SECRET_KEY')\napp.config['SQLALCHEMY_DATABASE_URI'] = config.get('SQLALCHEMY_DATABASE_URI')\ndb = SQLAlchemy(app)\nlogin_manager = LoginManager()\nlogin_manager.init_app(app)\nfrom flask_app import routes\nEND\n\n# Install and Configure Gunicorn\napt install -y gunicorn3\ngunicorn3 --workers=3 flask_app:app &\n\n# Install and Configure Supervisor\napt install -y supervisor\ncat < /etc/supervisor/conf.d/flask_app.conf\n[program:flask_app]\ndirectory=/home/flask_app_project\ncommand=gunicorn3 --workers=3 flask_app:app\nautostart=true\nautorestart=true\nstopasgroup=true\nkillasgroup=true\nstderr_logfile=/var/log/flask_app/flask_app.err.log\nstdout_logfile=/var/log/flask_app/flask_app.out.log\nEND\n\nmkdir /var/log/flask_app\ntouch /var/log/flask_app/flask_app.out.log\ntouch /var/log/flask_app/flask_app.err.log\nsupervisorctl reload\n\n# Cleanup\nstackscript_cleanup","user_defined_fields":[]},{"id":971045,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Focalboard One-Click","description":"Focalboard One-Click","ordinal":32,"logo_url":"assets/focalboard.svg","images":["linode/ubuntu22.04"],"deployments_total":489,"deployments_active":18,"is_public":true,"mine":false,"created":"2022-02-08T16:23:08","updated":"2024-01-10T18:58:12","rev_note":"","script":"#!/bin/bash\nset -e\ntrap \"cleanup $? $LINENO\" EXIT\n\n##Linode/SSH security settings\n#\n#\n#\n#\n\n## Domain Settings\n#\n#\n#\n\n## Focalboard Settings \n#\n\n\n# git repo\nexport GIT_REPO=\"https://github.com/akamai-compute-marketplace/marketplace-apps.git\"\nexport WORK_DIR=\"/tmp/marketplace-apps\" \nexport MARKETPLACE_APP=\"apps/linode-marketplace-focalboard\"\n\n# enable logging\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n\nfunction cleanup {\n if [ -d \"${WORK_DIR}\" ]; then\n rm -rf ${WORK_DIR}\n fi\n\n}\n\nfunction udf {\n\n local group_vars=\"${WORK_DIR}/${MARKETPLACE_APP}/group_vars/linode/vars\"\n\n echo \"webserver_stack: lemp\" >> ${group_vars};\n \n if [[ -n ${USER_NAME} ]]; then\n echo \"username: ${USER_NAME}\" >> ${group_vars};\n else echo \"No username entered\";\n fi\n\n if [ \"$DISABLE_ROOT\" = \"Yes\" ]; then\n echo \"disable_root: yes\" >> ${group_vars};\n else echo \"Leaving root login enabled\";\n fi\n\n if [[ -n ${PASSWORD} ]]; then\n echo \"password: ${PASSWORD}\" >> ${group_vars};\n else echo \"No password entered\";\n fi\n\n if [[ -n ${PUBKEY} ]]; then\n echo \"pubkey: ${PUBKEY}\" >> ${group_vars};\n else echo \"No pubkey entered\";\n fi\n\n #Focalboard vars\n \n if [[ -n ${SOA_EMAIL_ADDRESS} ]]; then\n echo \"soa_email_address: ${SOA_EMAIL_ADDRESS}\" >> ${group_vars};\n fi\n\n if [[ -n ${DOMAIN} ]]; then\n echo \"domain: ${DOMAIN}\" >> ${group_vars};\n else\n echo \"default_dns: $(hostname -I | awk '{print $1}'| tr '.' '-' | awk {'print $1 \".ip.linodeusercontent.com\"'})\" >> ${group_vars};\n fi\n\n if [[ -n ${SUBDOMAIN} ]]; then\n echo \"subdomain: ${SUBDOMAIN}\" >> ${group_vars};\n else echo \"subdomain: www\" >> ${group_vars};\n fi\n\n if [[ -n ${TOKEN_PASSWORD} ]]; then\n echo \"token_password: ${TOKEN_PASSWORD}\" >> ${group_vars};\n else echo \"No API token entered\";\n fi\n\n\n}\n\nfunction run {\n # install dependancies\n apt-get update\n apt-get install -y git python3 python3-pip\n\n # clone repo and set up ansible environment\n git -C /tmp clone ${GIT_REPO}\n # for a single testing branch\n # git -C /tmp clone -b ${BRANCH} ${GIT_REPO}\n\n # venv\n cd ${WORK_DIR}/${MARKETPLACE_APP}\n pip3 install virtualenv\n python3 -m virtualenv env\n source env/bin/activate\n pip install pip --upgrade\n pip install -r requirements.txt\n ansible-galaxy install -r collections.yml\n \n\n # populate group_vars\n udf\n # run playbooks\n for playbook in site.yml; do ansible-playbook -v $playbook; done\n \n}\n\nfunction installation_complete {\n echo \"Installation Complete\"\n}\n# main\nrun && installation_complete\ncleanup","user_defined_fields":[{"name":"user_name","label":"The limited sudo user to be created for the Linode","default":""},{"name":"password","label":"The password for the limited sudo user","example":"an0th3r_s3cure_p4ssw0rd","default":""},{"name":"disable_root","label":"Disable root access over SSH?","oneof":"Yes,No","default":"No"},{"name":"pubkey","label":"The SSH Public Key that will be used to access the Linode (Recommended)","default":""},{"name":"token_password","label":"Your Linode API token. This is needed to create your DNS records","default":""},{"name":"subdomain","label":"Subdomain","example":"The subdomain for the DNS record: www (Requires Domain)","default":""},{"name":"domain","label":"Domain","example":"The domain for the DNS record: example.com (Requires API token)","default":""},{"name":"soa_email_address","label":"Email address (for the Let's Encrypt SSL certificate)","example":"user@domain.tld"}]},{"id":1088136,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Galera Cluster One-Click","description":"Galera Cluster One-Click","ordinal":33,"logo_url":"assets/galeramarketplaceocc.svg","images":["linode/ubuntu22.04"],"deployments_total":154,"deployments_active":9,"is_public":true,"mine":false,"created":"2022-11-15T20:41:27","updated":"2024-01-10T18:35:30","rev_note":"","script":"#!/bin/bash\nset -e\ntrap \"cleanup $? $LINENO\" EXIT\n\n## Deployment Variables\n# \n# \n# \n# \n# \n# \n# \n# \n# \n# \n# \n# \n\n# git repo\nexport GIT_REPO=\"https://github.com/akamai-compute-marketplace/galera-occ\"\n\n# enable logging\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n# source script libraries\nsource \nfunction cleanup {\n if [ \"$?\" != \"0\" ] || [ \"$SUCCESS\" == \"true\" ]; then\n #deactivate\n cd ${HOME}\n if [ -d \"/tmp/galera-occ\" ]; then\n rm -rf /tmp/galera-occ\n fi\n if [ -d \"/usr/local/bin/run\" ]; then\n rm /usr/local/bin/run\n fi\n stackscript_cleanup\n fi\n}\nfunction add_privateip {\n curl -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer ${TOKEN_PASSWORD}\" \\\n -X POST -d '{\n \"type\": \"ipv4\",\n \"public\": false\n }' \\\n https://api.linode.com/v4/linode/instances/${LINODE_ID}/ips\n}\nfunction get_privateip {\n curl -s -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer ${TOKEN_PASSWORD}\" \\\n https://api.linode.com/v4/linode/instances/${LINODE_ID}/ips | \\\n jq -r '.ipv4.private[].address'\n}\nfunction configure_privateip {\n LINODE_IP=$(get_privateip)\n if [ ! -z \"${LINODE_IP}\" ]; then\n echo \"[+] Linode private IP present\"\n else\n echo \"[!] No private IP found. Adding..\"\n add_privateip\n LINODE_IP=$(get_privateip)\n ip addr add ${LINODE_IP}/17 dev eth0 label eth0:1\n fi\n}\nfunction rename_provisioner {\n INSTANCE_PREFIX=$(curl -sH \"Authorization: Bearer ${TOKEN_PASSWORD}\" \"https://api.linode.com/v4/linode/instances/${LINODE_ID}\" | jq -r .label)\n export INSTANCE_PREFIX=\"${INSTANCE_PREFIX}\"\n echo \"[+] renaming the provisioner\"\n curl -s -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer ${TOKEN_PASSWORD}\" \\\n -X PUT -d \"{\n \\\"label\\\": \\\"${INSTANCE_PREFIX}1\\\"\n }\" \\\n https://api.linode.com/v4/linode/instances/${LINODE_ID}\n}\nfunction setup {\n export DEBIAN_FRONTEND=non-interactive\n # install dependancies\n apt-get update && apt-get upgrade -y\n apt-get install -y jq git python3 python3-pip python3-dev build-essential firewalld\n # write authorized_keys file\n if [ \"${ADD_SSH_KEYS}\" == \"yes\" ]; then\n if [ ! -d ~/.ssh ]; then \n mkdir ~/.ssh\n else \n echo \".ssh directory is already created\"\n fi\n curl -sH \"Content-Type: application/json\" -H \"Authorization: Bearer ${TOKEN_PASSWORD}\" https://api.linode.com/v4/profile/sshkeys | jq -r .data[].ssh_key > /root/.ssh/authorized_keys\n fi\n # add private IP address\n rename_provisioner\n configure_privateip \n # clone repo and set up ansible environment\n git clone ${GIT_REPO} /tmp/galera-occ\n cd /tmp/galera-occ/\n pip3 install virtualenv\n python3 -m virtualenv env\n source env/bin/activate\n pip3 install -r requirements.txt\n ansible-galaxy install -r collections.yml\n # copy run script to path\n cp scripts/run.sh /usr/local/bin/run\n}\n# main\nsetup\nrun ansible:build\nrun ansible:deploy && export SUCCESS=\"true\"","user_defined_fields":[{"name":"cluster_name","label":"Cluster Name"},{"name":"token_password","label":"Your Linode API token"},{"name":"add_ssh_keys","label":"Add Account SSH Keys to All Nodes?","oneof":"yes,no"},{"name":"sslheader","label":"SSL Information","header":"Yes","default":"Yes","required":"Yes"},{"name":"country_name","label":"Details for self-signed SSL certificates: Country or Region","oneof":"AD,AE,AF,AG,AI,AL,AM,AO,AQ,AR,AS,AT,AU,AW,AX,AZ,BA,BB,BD,BE,BF,BG,BH,BI,BJ,BL,BM,BN,BO,BQ,BR,BS,BT,BV,BW,BY,BZ,CA,CC,CD,CF,CG,CH,CI,CK,CL,CM,CN,CO,CR,CU,CV,CW,CX,CY,CZ,DE,DJ,DK,DM,DO,DZ,EC,EE,EG,EH,ER,ES,ET,FI,FJ,FK,FM,FO,FR,GA,GB,GD,GE,GF,GG,GH,GI,GL,GM,GN,GP,GQ,GR,GS,GT,GU,GW,GY,HK,HM,HN,HR,HT,HU,ID,IE,IL,IM,IN,IO,IQ,IR,IS,IT,JE,JM,JO,JP,KE,KG,KH,KI,KM,KN,KP,KR,KW,KY,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,LY,MA,MC,MD,ME,MF,MG,MH,MK,ML,MM,MN,MO,MP,MQ,MR,MS,MT,MU,MV,MW,MX,MY,MZ,NA,NC,NE,NF,NG,NI,NL,NO,NP,NR,NU,NZ,OM,PA,PE,PF,PG,PH,PK,PL,PM,PN,PR,PS,PT,PW,PY,QA,RE,RO,RS,RU,RW,SA,SB,SC,SD,SE,SG,SH,SI,SJ,SK,SL,SM,SN,SO,SR,SS,ST,SV,SX,SY,SZ,TC,TD,TF,TG,TH,TJ,TK,TL,TM,TN,TO,TR,TT,TV,TW,TZ,UA,UG,UM,US,UY,UZ,VA,VC,VE,VG,VI,VN,VU,WF,WS,YE,YT,ZA,ZM,ZW"},{"name":"state_or_province_name","label":"State or Province","example":"Example: Pennsylvania"},{"name":"locality_name","label":"Locality","example":"Example: Philadelphia"},{"name":"organization_name","label":"Organization","example":"Example: Akamai Technologies"},{"name":"email_address","label":"Email Address","example":"Example: user@domain.tld"},{"name":"ca_common_name","label":"CA Common Name","default":"Galera CA"},{"name":"common_name","label":"Common Name","default":"Galera Server"},{"name":"cluster_size","label":"Galera cluster size","default":"3","oneof":"3"}]},{"id":688911,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"Gitea One-Click","description":"Gitea One-Click","ordinal":34,"logo_url":"assets/gitea.svg","images":["linode/debian10"],"deployments_total":1039,"deployments_active":65,"is_public":true,"mine":false,"created":"2020-11-17T21:16:09","updated":"2024-01-06T05:52:36","rev_note":"","script":"#! /bin/bash\n\n## Database Settings\n#\n#\n\n## User and SSH Security\n#\n#\n#\n#\n#\n#\n#\n\n## Domain Settings\n#\n#\n#\n#\n#\n#\n#\n#\n\nsource \nsource \nsource \nsource \n\nexec > >(tee /dev/ttyS0 /var/log/stackscript.log) 2>&1\n\n#assigns var for IP address\nreadonly ip=$(hostname -I | awk '{print$1}')\n\n#intall git\napt install -y git\n\n#install nginx\napt install -y nginx\n\n#install mysql and secure\nmysql_root_preinstall\napt-get install -y mariadb-server\nsystemctl start mariadb\nsystemctl enable mariadb\nrun_mysql_secure_installation\n\n#create mysql db and user\nmysql -u root --password=\"$DBROOT_PASSWORD\" -e \"CREATE DATABASE gitea;\"\nmysql -u root --password=\"$DBROOT_PASSWORD\" -e \"CREATE USER 'gitea'@'localhost' IDENTIFIED BY '$(printf '%q' \"$DB_PASSWORD\")';\"\nmysql -u root --password=\"$DBROOT_PASSWORD\" -e \"GRANT ALL PRIVILEGES ON gitea.* TO 'gitea'@'localhost' WITH GRANT OPTION;\"\nmysql -u root --password=\"$DBROOT_PASSWORD\" -e \"FLUSH PRIVILEGES;\"\n\n#create user for gitea\nadduser --system --disabled-password --group --shell /bin/bash --gecos 'Git Version Control' --home /home/git git\n\n#create directories for gitea\nmkdir -p /var/lib/gitea/{custom,data,log}\nchown -R git:git /var/lib/gitea/\nchmod -R 750 /var/lib/gitea/\nmkdir /etc/gitea\nchown root:git /etc/gitea\nchmod 770 /etc/gitea\n\n#pull down gitea binary\nwget -O gitea https://dl.gitea.io/gitea/1.13.0/gitea-1.13.0-linux-amd64\nchmod +x gitea\n\n#validate gpg\napt install gnupg -y\ngpg --keyserver keys.openpgp.org --recv 7C9E68152594688862D62AF62D9AE806EC1592E2\ngpg --verify gitea-1.13.0-linux-amd64.asc gitea-1.13.0-linux-amd64\n\n#copy gitea to global location\ncp gitea /usr/local/bin/gitea\n\n#download systemd file from gitea\nwget https://raw.githubusercontent.com/go-gitea/gitea/master/contrib/systemd/gitea.service -P /etc/systemd/system/\n\n#add requires mysql to the systemd file\nsed -i 's/#Requires=mariadb.service/Requires=mariadb.service/' /etc/systemd/system/gitea.service\n\n#start gitea as systemd service\nsystemctl daemon-reload\nsystemctl start gitea\nsystemctl enable gitea\n\n#configures ufw rules before nginx\nsystemctl start ufw\nufw allow http\nufw allow https\nufw enable\n\n#set absolute domain if any, otherwise use localhost\nif [[ $DOMAIN = \"\" ]]; then\n readonly ABS_DOMAIN=localhost\nelif [[ $SUBDOMAIN = \"\" ]]; then\n readonly ABS_DOMAIN=\"$DOMAIN\"\nelse\n readonly ABS_DOMAIN=\"$SUBDOMAIN.$DOMAIN\"\nfi\n\n#configure nginx reverse proxy\nrm /etc/nginx/sites-enabled/default\ntouch /etc/nginx/sites-available/reverse-proxy.conf\ncat < /etc/nginx/sites-available/reverse-proxy.conf\nserver {\n listen 80;\n listen [::]:80;\n server_name ${ABS_DOMAIN};\n\n access_log /var/log/nginx/reverse-access.log;\n error_log /var/log/nginx/reverse-error.log;\n\n location / {\n proxy_pass http://localhost:3000;\n }\n}\nEND\nln -s /etc/nginx/sites-available/reverse-proxy.conf /etc/nginx/sites-enabled/reverse-proxy.conf\n\n#enable and start nginx\nsystemctl enable nginx\nsystemctl restart nginx\n\nsleep 60\n\n#sets certbot ssl\nif [[ $SSL = \"Yes\" ]]; then\n check_dns_propagation ${ABS_DOMAIN} ${ip}\n apt install python3-certbot-nginx -y\n certbot run --non-interactive --nginx --agree-tos --redirect -d ${ABS_DOMAIN} -m ${EMAIL_ADDRESS} -w /var/www/html/\nfi\n\nstackscript_cleanup","user_defined_fields":[{"name":"dbroot_password","label":"MySQL root Password"},{"name":"db_password","label":"gitea Database Password"},{"name":"username","label":"The limited sudo user to be created for the Linode","default":""},{"name":"password","label":"The password for the limited sudo user","default":""},{"name":"pubkey","label":"The SSH Public Key that will be used to access the Linode","default":""},{"name":"pwless_sudo","label":"Enable passwordless sudo access for the limited user?","oneof":"Yes,No","default":"No"},{"name":"disable_root","label":"Disable root access over SSH?","oneof":"Yes,No","default":"No"},{"name":"auto_updates","label":"Configure automatic security updates?","oneof":"Yes,No","default":"No"},{"name":"fail2ban","label":"Use fail2ban to prevent automated instrusion attempts?","oneof":"Yes,No","default":"No"},{"name":"token_password","label":"Your Linode API token. This is needed to create your DNS records.","default":""},{"name":"subdomain","label":"The subdomain for your server (Domain required)","default":""},{"name":"domain","label":"Your domain (API Token required)","default":""},{"name":"soa_email_address","label":"SOA Email for your domain (Required for new domains)","default":""},{"name":"mx","label":"Do you need an MX record for this domain? (Yes if sending mail from this Linode)","oneof":"Yes,No","default":"No"},{"name":"spf","label":"Do you need an SPF record for this domain? (Yes if sending mail from this Linode)","oneof":"Yes,No","default":"No"},{"name":"ssl","label":"Would you like to use a free Let's Encrypt SSL certificate for your domain?","oneof":"Yes,No","default":"No"},{"name":"email_address","label":"Admin Email for Let's Encrypt certificate","default":""}]},{"id":401707,"username":"linode","user_gravatar_id":"9d4d301385af69ceb7ad658aad09c142","label":"GitLab One-Click","description":"GitLab One-Click","ordinal":35,"logo_url":"assets/GitLab.svg","images":["linode/ubuntu20.04","linode/debian11"],"deployments_total":3315,"deployments_active":139,"is_public":true,"mine":false,"created":"2019-03-08T21:12:21","updated":"2024-01-08T07:15:09","rev_note":"Remove SSH Pubkey UDF","script":"#!/usr/bin/env bash\n\n## Gitlab Settings\n#\n\n## Linode/SSH Security Settings\n#\n#\n#\n#\n\n## Domain Settings\n#\n#\n#
Installing...Get back after 3 minutes!