diff --git a/.docs/craig-code-engine.md b/.docs/craig-code-engine.md index 84b81a9d..31c76092 100644 --- a/.docs/craig-code-engine.md +++ b/.docs/craig-code-engine.md @@ -44,7 +44,13 @@ If you do not want Power VS workspaces created in every zone, you can create the #### generate-env.sh prerequisites - [jq](https://jqlang.github.io/jq/) v1.7 or higher - ibmcloud CLI -- Bash version 4 or higher + +#### Downloading generate-env.sh in IBM Cloud Shell +From within IBM Cloud Shell run the following two commands to download the script and make it executable: +```bash +wget https://raw.githubusercontent.com/IBM/CRAIG/main/generate-env.sh +chmod 755 generate-env.sh +``` To generate an env containing all of the workspaces in your account, you can run the following command: diff --git a/.docs/poc-template-worksheet.md b/.docs/poc-template-worksheet.md new file mode 100644 index 00000000..a618074e --- /dev/null +++ b/.docs/poc-template-worksheet.md @@ -0,0 +1,33 @@ +# Worksheet for Power VS POC template + +Set up access policies in your IBM Cloud account following the instructions [here](access-policies.md)
+ +Create in your IBM cloud account:
+- API key + +## Record the values for the keys mentioned below: + +### On-Premises information + +| *Keys* | *Values* | +| ------------------------------------------------------------------------------------------- | -------- | +| [*On-Prem network CIDRs](powervs-poc.md#on-premises-network-cidrs-and-peer-address) | | +| [*On-Prem Peer Address](powervs-poc.md#on-premises-network-cidrs-and-peer-address) | | +| [*On-Prem connection preshared key](powervs-poc.md#configuring-the-on-premises-vpn-gateway) | | + +### IBM Cloud information + +| *Keys* | *Values* | +| -------------------------------------------------------------------------------------------------------------- | -------- | +| [Region](powervs-poc.md#region-and-power-vs-zone) | | +| [Power VS Zone](powervs-poc.md#region-and-power-vs-zone) | | +| [resource name prefix](powervs-poc.md#resource-prefix) (optional) | | +| [*Public SSH key](powervs-poc.md#set-public-ssh-keys) | | +| [*IBM Cloud API Key](https://cloud.ibm.com/docs/account?topic=account-userapikey&interface=ui#create_user_key) | | +| [VPC VPN network CIDR](powervs-poc.md#vpc-network-cidr) | | +| [VPC VSI network CIDR](powervs-poc.md#vpc-network-cidr) | | +| [VPC VPE network CIDR](powervs-poc.md#vpc-network-cidr) | | +| [Power VS network CIDR](powervs-poc.md#power-virtual-server-network-cidr) | | + + +*Note*: The asterisk (*) denotes mandatory fields; optional values can be provided, otherwise CRAIG will default to predefined values. \ No newline at end of file diff --git a/.docs/power-vs-workspace-deployment.md b/.docs/power-vs-workspace-deployment.md index fa6e4a80..0b35e781 100644 --- a/.docs/power-vs-workspace-deployment.md +++ b/.docs/power-vs-workspace-deployment.md @@ -31,7 +31,6 @@ If you do not want Power VS workspaces created in every zone or if you want to u #### generate-env.sh prerequisites - [jq](https://jqlang.github.io/jq/) v1.7 or higher - [IBM Cloud CLI](https://cloud.ibm.com/docs/cli?topic=cli-getting-started) -- Bash version 4 or higher To generate a `.env` file containing all of the workspaces in your account, you can run the following command: diff --git a/.docs/powervs-poc.md b/.docs/powervs-poc.md index c0297c9b..18dbcfda 100644 --- a/.docs/powervs-poc.md +++ b/.docs/powervs-poc.md @@ -77,7 +77,7 @@ After changing the region and zone(s) in the options, the existing Power VS work The default AIX and IBM i virtual servers in the template will also need to be updated with the newly selected images. Click on each of the red Virtual Server icons, set their image, and click the save button for the VSI. ### VPC network CIDR -To change CIDR of the VPC networks, click on `VPC Networks` on the left navigation bar. Click on the the network you would like to update, change the "Advanced Configuration" to true, de-select zones 2 and 3 from the Zones seletor and press the Save button. +To change CIDR of the VPC networks, click on `VPC Networks` on the left navigation bar. Click on the the network you would like to update, change the "Advanced Configuration" to true, de-select zones 2 and 3 from the Zones selector and press the Save button. Finally, change the subnet CIDR and click the Save button next to the subnet name. diff --git a/.docs/schematics-how-to.md b/.docs/schematics-how-to.md index 8df0217a..7b28515e 100644 --- a/.docs/schematics-how-to.md +++ b/.docs/schematics-how-to.md @@ -8,8 +8,8 @@ The `API_KEY` variable must be set in the `.env`. If CRAIG is deployed in IBM Co ### Access Policy In order to allow Schematics integration, users should make sure they have the following access policy roles for the Schematics service: ->* `Editor` or greater Platform access >* `Writer` or greater Service access +>* `Editor` or greater Platform access These roles allow the integration with Schematics including the Schematics workspace creation and the upload of the project. However, to create and manage the IBM Cloud resources in the template, you must be assigned the IAM platform or service access role for the individual IBM Cloud resources that are in the template. See the IBM Cloud documentation for the various services for specific roles required. diff --git a/CHANGELOG.md b/CHANGELOG.md index a47d8bc7..fd7a8f6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,33 @@ All notable changes to this project will be documented in this file. +## 1.13.0 + +### Upgrade Notes + +- A new, improved version of the CRAIG tutorial is available in the README. This tutorial shows users how to install CRAIG, use the new V2 GUI, and integrate deployments with schematics +- Deprecated field for VSI primary IPs has been change to a version that no longer throws Terraform warnings + +### Features + +- Users will now get feedback when a Power VS Workspace has no VTL images when trying to create or update a VTL instance. Images can be added from the Power VS Workspace form +- Icons for deployments within imported subnets are now rendered within that subnet on the `/v2/vpcDeployments` page +- Users can now create Classic VSI from the form page `/form/classicVsi` +- Power VS Workspace names, ids, and CRNs are now included as outputs in the `outputs.tf` file of any CRAIG Terraform template +- Power VS High Availability is now supported for `mad02`, `mad04`, `us-east`, `wdc06`, `us-south`, `eu-de-1`, and `eu-de-2` +- When updating a Power VS Instance name, references to that instance are now updated to match the new name +- Users can now upload JSON directly to CRAIG from the local file explorer from the Projects page by clicking the new `Upload JSON` button +- Users can now create Classic Bare Metal Servers from the form page `/form/classicBareMetal` +- Power VS Instance Primary IP addresses are now included as outputs in the `outputs.tf` file of any CRAIG Terraform template +- When downloading a `.zip` file of an environment from the CRAIG GUI, an image of the current environment is now included in the archive +- CRAIG now supports adding a Pin Policy to Power VS instances + +### Fixes + +- Fixed an issue with button hoverText alignment causing overflow in forms +- Fixed an issue causing an incorrect name for DNS Services in the `/v2/services` page +- Fixed an issue preventing users from inputing decimal values for CPU when creating a FalconStor VTL instance with a shared processor type + ## 1.12.2 ### Upgrade Notes diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..de488d01 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +email. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..f57fd948 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,28 @@ +# Contributing + +Found a bug or need an additional feature? File an issue in this repository with the following information and they will be responded to in a timely manner. + +## Bugs + +- A detailed title describing the issue with the current release and the tag `[BUG]`. For sprint one, filing a bug would have the title `[0.1.0][BUG] ` +- Steps to recreate said bug (including non-sensitive variables) +- (optional) Corresponding output logs **as text or as part of a code block** +- Tag bug issues with the `bug` label +- If you come across a vulnerability that needs to be addressed immediately, use the `vulnerability` label + + +## Features + +- A detailed title describing the desired feature that includes the current release. For sprint one, a feature would have the title `[0.1.0] ` +- A detailed description including the user story +- A checkbox list of needed features +- Tag the issue with the `enhancement` label + +Want to work on an issue? Be sure to assign it to yourself and branch from main. When you're done making the required changes, create a pull request. + +## Pull requests + +**Do not merge directly to main**. Pull requests should reference the corresponding issue filed in this repository. Please be sure to maintain **code coverage** before merging. + +At least **two** reviews are required to merge a pull request. When creating a pull request, please ensure that details about unexpected changes to the codebase are provided in the description. + diff --git a/README.md b/README.md index 2a51f2ef..ba11be9e 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,16 @@ CRAIG configures infrastructure using JSON to create full VPC networks, manage s --- +### Tutorial Video + +[Follow this tutorial](https://ibm.box.com/v/craigTutorialVideo) for step-by-step instructions on how to get started with CRAIG. + +***Ensure `Quality: 1080p` is selected within Box video player settings for the best viewing experience.*** + +***Last Updated: March 11th, 2024*** + +--- + ## Installation 1. [Running CRAIG Application Locally](#running-craig-application-locally) @@ -27,14 +37,6 @@ CRAIG configures infrastructure using JSON to create full VPC networks, manage s --- -### Tutorial Video - -[Follow this tutorial](https://ibm.box.com/v/craigTutorialVideo) for step-by-step instructions on how to get started with CRAIG. - -***Ensure `Quality: 1080p` is selected within Box video player settings for the best viewing experience.*** - ---- - ### Running CRAIG Application Locally To get started using CRAIG locally, follow these steps: @@ -74,12 +76,12 @@ Within the root directory is a script `deploy.sh` which deploys CRAIG to IBM Clo Users should make sure they have the following access policy roles for the IBM Code Engine service set within their IBM Cloud Account: -- IBM Cloud Platform Roles: Editor or Higher -- Code Engine Service Roles: Writer or Higher +>* `Writer` or greater Service access +>* `Editor` or greater Platform access Users should also make sure they have the following access policy roles for the IBM Cloud Container Registry service set within their IBM Cloud Account: -- Container Registry Service: Manager +>* `Manager` Service access These permissions are the minimum requirements needed in order to provision a Code Engine project, Container Registry namespace, application, image build, and secrets using the `deploy.sh` script. @@ -108,7 +110,14 @@ chmod 755 deploy.sh By default the script will securely prompt you for your API key. It may also be read from an environment variable or specified as a command line argument. See the `deploy.sh -h` usage for more information. - If CRAIG is used for Power VS configuration, Power VS workspaces must exist in the zones that CRAIG projects will use. The deploy script can create the Power Virtual Server workspaces in every Power VS zone worldwide and automatically integrate them with the CRAIG deployment. The deploy script uses a Schematics workspace and Terraform to drive the creation and deletion of the Power Virtual Server workspaces. Specify the `-z` parameter to automatically create the Power Virtual Server workspaces: + If CRAIG is used for Power VS configuration, Power Virtual Server workspaces must exist in the zones that CRAIG projects will use. The deploy script can create the Power Virtual Server workspaces in every Power VS zone worldwide and automatically integrate them with the CRAIG deployment. + + The deploy script uses a Schematics workspace and Terraform to drive the creation and deletion of the Power Virtual Server workspaces. In order to allow Schematics integration, users should make sure they have the following access policy roles for the Schematics service set within their IBM Cloud Account: + +>* `Writer` or greater Service access +>* `Editor` or greater Platform access + + Once access policy roles for the Schematics service are properly configured, users can specify the `-z` parameter to automatically create the Power Virtual Server workspaces alongside your CRAIG deployment: ```bash ./deploy.sh -z @@ -161,9 +170,11 @@ Make sure to set the `API_KEY` variable in a `.env` file to be used for IBM Clou See `.env.example` found [here](./.env.example) ```shell -docker run -it --env-file .env -- craig +docker run -t -d -p 8080:8080 --env-file .env -- craig ``` +CRAIG is now available at http://localhost:8080. + --- ### Setting Up CRAIG Development Environment diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..d498ccee --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,12 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ---------- | ------------------- | +| > 1.10.x | :white_check_mark: | +| < 1.10.0 | :x: | + +## Reporting a Vulnerability + +- Create a GitHub issue and use the `vulnerability` label \ No newline at end of file diff --git a/ansible/template-test/main.yml b/ansible/template-test/main.yml index 18405485..912272c9 100644 --- a/ansible/template-test/main.yml +++ b/ansible/template-test/main.yml @@ -10,25 +10,9 @@ - name: Upload CRAIG template to schematics workspace hosts: localhost vars_files: ./vars/vars.yml -- name: Get IAM token - hosts: localhost - vars_files: ./vars/vars.yml # variables declared in variables file are added to role automatically roles: - role: get_iam_token -- name: "Download Template Tarball" - hosts: localhost - vars_files: ./vars/vars.yml - tasks: - - name: Download {{template}}.tar to current directory - get_url: - url: "{{craig_url}}/{{template}}" - dest: "{{playbook_dir}}/{{template}}.tar" - async: 120 - retries: 10 -- name: Create Schematics Workspace - hosts: localhost - vars_files: ./vars/vars.yml # variables declared in variables file are added to role automatically - roles: + - role: download_tar - role: create_schematics_workspace vars: description: Automated CRAIG Testing Workspace @@ -69,70 +53,36 @@ body_format: json body: variablestore: "{{ variablestore }}" - - name: Start generate plan action - uri: - url: https://schematics.cloud.ibm.com/v1/workspaces/{{ workspace.json.id }}/plan - method: POST - body_format: json - headers: - Authorization: Bearer {{token.json.access_token}} - status_code: 202 - register: job - - name: Ensure generate plan finishes - uri: - url: https://schematics.cloud.ibm.com/v2/jobs/{{job.json.activityid}} - method: GET - body_format: json - headers: - Authorization: Bearer {{token.json.access_token}} - register: plan - until: plan.json.status.workspace_job_status.status_code == "job_finished" or plan.json.status.workspace_job_status.status_code == "job_failed" - failed_when: plan.json.status.workspace_job_status.status_code == "job_failed" - delay: 90 - retries: 50 - - name: Start apply plan action - uri: - url: https://schematics.cloud.ibm.com/v1/workspaces/{{workspace.json.id}}/apply - method: PUT - body_format: json - headers: - Authorization: Bearer {{token.json.access_token}} - status_code: 202 - register: apply - - name: Ensure apply plan finishes - uri: - url: https://schematics.cloud.ibm.com/v2/jobs/{{apply.json.activityid}} - method: GET - body_format: json - headers: - Authorization: Bearer {{token.json.access_token}} - register: apply_plan - until: apply_plan.json.status.workspace_job_status.status_code == "job_finished" or apply_plan.json.status.workspace_job_status.status_code == "job_failed" - failed_when: apply_plan.json.status.workspace_job_status.status_code == "job_failed" - delay: 120 - retries: 50 - - name: Start destroy action - uri: - url: https://schematics.cloud.ibm.com/v1/workspaces/{{workspace.json.id}}/destroy - method: PUT - body_format: json - headers: - Authorization: Bearer {{token.json.access_token}} - status_code: 202 - register: destroy - - name: Ensure destory finishes - uri: - url: https://schematics.cloud.ibm.com/v2/jobs/{{destroy.json.activityid}} - method: GET - body_format: json - headers: - Authorization: Bearer {{token.json.access_token}} - register: destroy_plan - until: destroy_plan.json.status.workspace_job_status.status_code == "job_finished" or destroy_plan.json.status.workspace_job_status.status_code == "job_failed" - failed_when: destroy_plan.json.status.workspace_job_status.status_code == "job_failed" - delay: 120 - retries: 50 - - name: Delete local {{template}}.tar file - file: - state: absent - path: /{{playbook_dir}}/{{template}}.tar \ No newline at end of file +- name: Start generate plan action + hosts: localhost + vars_files: ./vars/vars.yml + roles: + - role: get_iam_token + - role: action + vars: + action: plan + - role: get_iam_token + - role: await_action + vars: + action: plan + register_name: await_plan + - role: get_iam_token + - role: action + vars: + action: apply + - role: get_iam_token + - role: await_action + vars: + action: apply + register_name: await_apply + - role: get_iam_token + - role: action + vars: + action: destroy + - role: get_iam_token + - role: await_action + vars: + action: destroy + register_name: await_destroy + - role: get_iam_token + - role: delete_schematics_workspace \ No newline at end of file diff --git a/ansible/template-test/roles/action/README.md b/ansible/template-test/roles/action/README.md new file mode 100644 index 00000000..225dd44b --- /dev/null +++ b/ansible/template-test/roles/action/README.md @@ -0,0 +1,38 @@ +Role Name +========= + +A brief description of the role goes here. + +Requirements +------------ + +Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. + +Role Variables +-------------- + +A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. + +Dependencies +------------ + +A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. + +Example Playbook +---------------- + +Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: + + - hosts: servers + roles: + - { role: username.rolename, x: 42 } + +License +------- + +BSD + +Author Information +------------------ + +An optional section for the role authors to include contact information, or a website (HTML is not allowed). diff --git a/ansible/template-test/roles/action/defaults/main.yml b/ansible/template-test/roles/action/defaults/main.yml new file mode 100644 index 00000000..0686718f --- /dev/null +++ b/ansible/template-test/roles/action/defaults/main.yml @@ -0,0 +1,2 @@ +--- +# defaults file for action diff --git a/ansible/template-test/roles/action/handlers/main.yml b/ansible/template-test/roles/action/handlers/main.yml new file mode 100644 index 00000000..830712d2 --- /dev/null +++ b/ansible/template-test/roles/action/handlers/main.yml @@ -0,0 +1,2 @@ +--- +# handlers file for action diff --git a/ansible/template-test/roles/action/meta/main.yml b/ansible/template-test/roles/action/meta/main.yml new file mode 100644 index 00000000..c572acc9 --- /dev/null +++ b/ansible/template-test/roles/action/meta/main.yml @@ -0,0 +1,52 @@ +galaxy_info: + author: your name + description: your role description + company: your company (optional) + + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Choose a valid license ID from https://spdx.org - some suggested licenses: + # - BSD-3-Clause (default) + # - MIT + # - GPL-2.0-or-later + # - GPL-3.0-only + # - Apache-2.0 + # - CC-BY-4.0 + license: license (GPL-2.0-or-later, MIT, etc) + + min_ansible_version: 2.1 + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + # + # Provide a list of supported platforms, and for each platform a list of versions. + # If you don't wish to enumerate all versions for a particular platform, use 'all'. + # To view available platforms and versions (or releases), visit: + # https://galaxy.ansible.com/api/v1/platforms/ + # + # platforms: + # - name: Fedora + # versions: + # - all + # - 25 + # - name: SomePlatform + # versions: + # - all + # - 1.0 + # - 7 + # - 99.99 + + galaxy_tags: [] + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. + +dependencies: [] + # List your role dependencies here, one per line. Be sure to remove the '[]' above, + # if you add dependencies to this list. diff --git a/ansible/template-test/roles/action/tasks/main.yml b/ansible/template-test/roles/action/tasks/main.yml new file mode 100644 index 00000000..9ad4bec3 --- /dev/null +++ b/ansible/template-test/roles/action/tasks/main.yml @@ -0,0 +1,11 @@ +--- +# tasks file for action +- name: Start {{ action }} action + uri: + url: https://schematics.cloud.ibm.com/v1/workspaces/{{ workspace.json.id }}/{{ action }} + method: "{{ 'POST' if action == 'plan' else 'PUT' }}" + body_format: json + headers: + Authorization: Bearer {{token.json.access_token}} + status_code: 202 + register: action_result \ No newline at end of file diff --git a/ansible/template-test/roles/action/tests/inventory b/ansible/template-test/roles/action/tests/inventory new file mode 100644 index 00000000..878877b0 --- /dev/null +++ b/ansible/template-test/roles/action/tests/inventory @@ -0,0 +1,2 @@ +localhost + diff --git a/ansible/template-test/roles/action/tests/test.yml b/ansible/template-test/roles/action/tests/test.yml new file mode 100644 index 00000000..a6919dbf --- /dev/null +++ b/ansible/template-test/roles/action/tests/test.yml @@ -0,0 +1,5 @@ +--- +- hosts: localhost + remote_user: root + roles: + - action diff --git a/ansible/template-test/roles/action/vars/main.yml b/ansible/template-test/roles/action/vars/main.yml new file mode 100644 index 00000000..5f086714 --- /dev/null +++ b/ansible/template-test/roles/action/vars/main.yml @@ -0,0 +1,2 @@ +--- +# vars file for action diff --git a/ansible/template-test/roles/await_action/README.md b/ansible/template-test/roles/await_action/README.md new file mode 100644 index 00000000..225dd44b --- /dev/null +++ b/ansible/template-test/roles/await_action/README.md @@ -0,0 +1,38 @@ +Role Name +========= + +A brief description of the role goes here. + +Requirements +------------ + +Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. + +Role Variables +-------------- + +A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. + +Dependencies +------------ + +A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. + +Example Playbook +---------------- + +Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: + + - hosts: servers + roles: + - { role: username.rolename, x: 42 } + +License +------- + +BSD + +Author Information +------------------ + +An optional section for the role authors to include contact information, or a website (HTML is not allowed). diff --git a/ansible/template-test/roles/await_action/defaults/main.yml b/ansible/template-test/roles/await_action/defaults/main.yml new file mode 100644 index 00000000..40b387ed --- /dev/null +++ b/ansible/template-test/roles/await_action/defaults/main.yml @@ -0,0 +1,2 @@ +--- +# defaults file for await_action diff --git a/ansible/template-test/roles/await_action/handlers/main.yml b/ansible/template-test/roles/await_action/handlers/main.yml new file mode 100644 index 00000000..a886d9d5 --- /dev/null +++ b/ansible/template-test/roles/await_action/handlers/main.yml @@ -0,0 +1,2 @@ +--- +# handlers file for await_action diff --git a/ansible/template-test/roles/await_action/meta/main.yml b/ansible/template-test/roles/await_action/meta/main.yml new file mode 100644 index 00000000..c572acc9 --- /dev/null +++ b/ansible/template-test/roles/await_action/meta/main.yml @@ -0,0 +1,52 @@ +galaxy_info: + author: your name + description: your role description + company: your company (optional) + + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Choose a valid license ID from https://spdx.org - some suggested licenses: + # - BSD-3-Clause (default) + # - MIT + # - GPL-2.0-or-later + # - GPL-3.0-only + # - Apache-2.0 + # - CC-BY-4.0 + license: license (GPL-2.0-or-later, MIT, etc) + + min_ansible_version: 2.1 + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + # + # Provide a list of supported platforms, and for each platform a list of versions. + # If you don't wish to enumerate all versions for a particular platform, use 'all'. + # To view available platforms and versions (or releases), visit: + # https://galaxy.ansible.com/api/v1/platforms/ + # + # platforms: + # - name: Fedora + # versions: + # - all + # - 25 + # - name: SomePlatform + # versions: + # - all + # - 1.0 + # - 7 + # - 99.99 + + galaxy_tags: [] + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. + +dependencies: [] + # List your role dependencies here, one per line. Be sure to remove the '[]' above, + # if you add dependencies to this list. diff --git a/ansible/template-test/roles/await_action/tasks/main.yml b/ansible/template-test/roles/await_action/tasks/main.yml new file mode 100644 index 00000000..969284ae --- /dev/null +++ b/ansible/template-test/roles/await_action/tasks/main.yml @@ -0,0 +1,15 @@ +- name: Ensure {{ action }} finishes + uri: + url: https://schematics.cloud.ibm.com/v2/jobs/{{ action_result.json.activityid }} + method: GET + body_format: json + headers: + Authorization: Bearer {{ token.json.access_token }} + register: dynamic_value + until: dynamic_value.json.status.workspace_job_status.status_code == "job_finished" or dynamic_value.json.status.workspace_job_status.status_code == "job_failed" + failed_when: dynamic_value.json.status.workspace_job_status.status_code == "job_failed" + delay: 120 + retries: 50 +- name: Set Variable + set_fact: + "{{ register_name }}": "{{ dynamic_value }}" \ No newline at end of file diff --git a/ansible/template-test/roles/await_action/tests/inventory b/ansible/template-test/roles/await_action/tests/inventory new file mode 100644 index 00000000..878877b0 --- /dev/null +++ b/ansible/template-test/roles/await_action/tests/inventory @@ -0,0 +1,2 @@ +localhost + diff --git a/ansible/template-test/roles/await_action/tests/test.yml b/ansible/template-test/roles/await_action/tests/test.yml new file mode 100644 index 00000000..83cf53f8 --- /dev/null +++ b/ansible/template-test/roles/await_action/tests/test.yml @@ -0,0 +1,5 @@ +--- +- hosts: localhost + remote_user: root + roles: + - await_action diff --git a/ansible/template-test/roles/await_action/vars/main.yml b/ansible/template-test/roles/await_action/vars/main.yml new file mode 100644 index 00000000..72bd7e8c --- /dev/null +++ b/ansible/template-test/roles/await_action/vars/main.yml @@ -0,0 +1,2 @@ +--- +# vars file for await_action diff --git a/ansible/template-test/roles/delete_schematics_workspace/README.md b/ansible/template-test/roles/delete_schematics_workspace/README.md new file mode 100644 index 00000000..225dd44b --- /dev/null +++ b/ansible/template-test/roles/delete_schematics_workspace/README.md @@ -0,0 +1,38 @@ +Role Name +========= + +A brief description of the role goes here. + +Requirements +------------ + +Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. + +Role Variables +-------------- + +A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. + +Dependencies +------------ + +A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. + +Example Playbook +---------------- + +Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: + + - hosts: servers + roles: + - { role: username.rolename, x: 42 } + +License +------- + +BSD + +Author Information +------------------ + +An optional section for the role authors to include contact information, or a website (HTML is not allowed). diff --git a/ansible/template-test/roles/delete_schematics_workspace/defaults/main.yml b/ansible/template-test/roles/delete_schematics_workspace/defaults/main.yml new file mode 100644 index 00000000..87646b0e --- /dev/null +++ b/ansible/template-test/roles/delete_schematics_workspace/defaults/main.yml @@ -0,0 +1,2 @@ +--- +# defaults file for delete_schematics_workspace diff --git a/ansible/template-test/roles/delete_schematics_workspace/handlers/main.yml b/ansible/template-test/roles/delete_schematics_workspace/handlers/main.yml new file mode 100644 index 00000000..3b193b92 --- /dev/null +++ b/ansible/template-test/roles/delete_schematics_workspace/handlers/main.yml @@ -0,0 +1,2 @@ +--- +# handlers file for delete_schematics_workspace diff --git a/ansible/template-test/roles/delete_schematics_workspace/meta/main.yml b/ansible/template-test/roles/delete_schematics_workspace/meta/main.yml new file mode 100644 index 00000000..c572acc9 --- /dev/null +++ b/ansible/template-test/roles/delete_schematics_workspace/meta/main.yml @@ -0,0 +1,52 @@ +galaxy_info: + author: your name + description: your role description + company: your company (optional) + + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Choose a valid license ID from https://spdx.org - some suggested licenses: + # - BSD-3-Clause (default) + # - MIT + # - GPL-2.0-or-later + # - GPL-3.0-only + # - Apache-2.0 + # - CC-BY-4.0 + license: license (GPL-2.0-or-later, MIT, etc) + + min_ansible_version: 2.1 + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + # + # Provide a list of supported platforms, and for each platform a list of versions. + # If you don't wish to enumerate all versions for a particular platform, use 'all'. + # To view available platforms and versions (or releases), visit: + # https://galaxy.ansible.com/api/v1/platforms/ + # + # platforms: + # - name: Fedora + # versions: + # - all + # - 25 + # - name: SomePlatform + # versions: + # - all + # - 1.0 + # - 7 + # - 99.99 + + galaxy_tags: [] + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. + +dependencies: [] + # List your role dependencies here, one per line. Be sure to remove the '[]' above, + # if you add dependencies to this list. diff --git a/ansible/template-test/roles/delete_schematics_workspace/tasks/main.yml b/ansible/template-test/roles/delete_schematics_workspace/tasks/main.yml new file mode 100644 index 00000000..9a02962b --- /dev/null +++ b/ansible/template-test/roles/delete_schematics_workspace/tasks/main.yml @@ -0,0 +1,13 @@ +--- +# tasks file for delete_schematics_workspace +- name: Delete Schematics Workspace + uri: + url: https://schematics.cloud.ibm.com/v1/workspaces/{{workspace.json.id}} + method: DELETE + headers: + Authorization: Bearer {{token.json.access_token}} + status_code: 200 + register: delete_response + until: delete_response.status == 200 + retries: 2 + delay: 120 \ No newline at end of file diff --git a/ansible/template-test/roles/delete_schematics_workspace/tests/inventory b/ansible/template-test/roles/delete_schematics_workspace/tests/inventory new file mode 100644 index 00000000..878877b0 --- /dev/null +++ b/ansible/template-test/roles/delete_schematics_workspace/tests/inventory @@ -0,0 +1,2 @@ +localhost + diff --git a/ansible/template-test/roles/delete_schematics_workspace/tests/test.yml b/ansible/template-test/roles/delete_schematics_workspace/tests/test.yml new file mode 100644 index 00000000..3c1a8e6a --- /dev/null +++ b/ansible/template-test/roles/delete_schematics_workspace/tests/test.yml @@ -0,0 +1,5 @@ +--- +- hosts: localhost + remote_user: root + roles: + - delete_schematics_workspace diff --git a/ansible/template-test/roles/delete_schematics_workspace/vars/main.yml b/ansible/template-test/roles/delete_schematics_workspace/vars/main.yml new file mode 100644 index 00000000..7ae746a9 --- /dev/null +++ b/ansible/template-test/roles/delete_schematics_workspace/vars/main.yml @@ -0,0 +1,2 @@ +--- +# vars file for delete_schematics_workspace diff --git a/ansible/template-test/roles/download_tar/README.md b/ansible/template-test/roles/download_tar/README.md new file mode 100644 index 00000000..225dd44b --- /dev/null +++ b/ansible/template-test/roles/download_tar/README.md @@ -0,0 +1,38 @@ +Role Name +========= + +A brief description of the role goes here. + +Requirements +------------ + +Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. + +Role Variables +-------------- + +A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. + +Dependencies +------------ + +A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. + +Example Playbook +---------------- + +Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: + + - hosts: servers + roles: + - { role: username.rolename, x: 42 } + +License +------- + +BSD + +Author Information +------------------ + +An optional section for the role authors to include contact information, or a website (HTML is not allowed). diff --git a/ansible/template-test/roles/download_tar/defaults/main.yml b/ansible/template-test/roles/download_tar/defaults/main.yml new file mode 100644 index 00000000..e2612f9b --- /dev/null +++ b/ansible/template-test/roles/download_tar/defaults/main.yml @@ -0,0 +1,2 @@ +--- +# defaults file for download_tar diff --git a/ansible/template-test/roles/download_tar/handlers/main.yml b/ansible/template-test/roles/download_tar/handlers/main.yml new file mode 100644 index 00000000..8954f993 --- /dev/null +++ b/ansible/template-test/roles/download_tar/handlers/main.yml @@ -0,0 +1,2 @@ +--- +# handlers file for download_tar diff --git a/ansible/template-test/roles/download_tar/meta/main.yml b/ansible/template-test/roles/download_tar/meta/main.yml new file mode 100644 index 00000000..c572acc9 --- /dev/null +++ b/ansible/template-test/roles/download_tar/meta/main.yml @@ -0,0 +1,52 @@ +galaxy_info: + author: your name + description: your role description + company: your company (optional) + + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Choose a valid license ID from https://spdx.org - some suggested licenses: + # - BSD-3-Clause (default) + # - MIT + # - GPL-2.0-or-later + # - GPL-3.0-only + # - Apache-2.0 + # - CC-BY-4.0 + license: license (GPL-2.0-or-later, MIT, etc) + + min_ansible_version: 2.1 + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + # + # Provide a list of supported platforms, and for each platform a list of versions. + # If you don't wish to enumerate all versions for a particular platform, use 'all'. + # To view available platforms and versions (or releases), visit: + # https://galaxy.ansible.com/api/v1/platforms/ + # + # platforms: + # - name: Fedora + # versions: + # - all + # - 25 + # - name: SomePlatform + # versions: + # - all + # - 1.0 + # - 7 + # - 99.99 + + galaxy_tags: [] + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. + +dependencies: [] + # List your role dependencies here, one per line. Be sure to remove the '[]' above, + # if you add dependencies to this list. diff --git a/ansible/template-test/roles/download_tar/tasks/main.yml b/ansible/template-test/roles/download_tar/tasks/main.yml new file mode 100644 index 00000000..35686117 --- /dev/null +++ b/ansible/template-test/roles/download_tar/tasks/main.yml @@ -0,0 +1,8 @@ +--- +# tasks file for download_tar +- name: Download {{template}}.tar to current directory + get_url: + url: "{{craig_url}}/{{template}}" + dest: "{{playbook_dir}}/{{template}}.tar" + async: 120 + retries: 10 \ No newline at end of file diff --git a/ansible/template-test/roles/download_tar/tests/inventory b/ansible/template-test/roles/download_tar/tests/inventory new file mode 100644 index 00000000..878877b0 --- /dev/null +++ b/ansible/template-test/roles/download_tar/tests/inventory @@ -0,0 +1,2 @@ +localhost + diff --git a/ansible/template-test/roles/download_tar/tests/test.yml b/ansible/template-test/roles/download_tar/tests/test.yml new file mode 100644 index 00000000..a45b7871 --- /dev/null +++ b/ansible/template-test/roles/download_tar/tests/test.yml @@ -0,0 +1,5 @@ +--- +- hosts: localhost + remote_user: root + roles: + - download_tar diff --git a/ansible/template-test/roles/download_tar/vars/main.yml b/ansible/template-test/roles/download_tar/vars/main.yml new file mode 100644 index 00000000..c9373e95 --- /dev/null +++ b/ansible/template-test/roles/download_tar/vars/main.yml @@ -0,0 +1,2 @@ +--- +# vars file for download_tar diff --git a/client/package-lock.json b/client/package-lock.json index 1c1f469f..69abd4ee 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "craig", - "version": "1.12.1", + "version": "1.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "craig", - "version": "1.12.1", + "version": "1.13.0", "license": "Apache-2.0", "dependencies": { "@apollo/client": "^3.4.10", @@ -17,6 +17,7 @@ "chart.js": "^4.4.1", "file-saver": "^2.0.5", "graphql": "15.5.2", + "html-to-image": "^1.11.11", "json-to-tf": "^0.3.1", "jszip": "^3.10.1", "lazy-z": "^1.11.17", @@ -13378,6 +13379,11 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, + "node_modules/html-to-image": { + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.11.tgz", + "integrity": "sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==" + }, "node_modules/htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", diff --git a/client/package.json b/client/package.json index 6c879880..f0a5d410 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "craig", - "version": "1.12.1", + "version": "1.13.0", "private": true, "license": "Apache-2.0", "scripts": { @@ -23,6 +23,7 @@ "chart.js": "^4.4.1", "file-saver": "^2.0.5", "graphql": "15.5.2", + "html-to-image": "^1.11.11", "json-to-tf": "^0.3.1", "jszip": "^3.10.1", "lazy-z": "^1.11.17", diff --git a/client/src/Craig.js b/client/src/Craig.js index b77337bf..49d15869 100644 --- a/client/src/Craig.js +++ b/client/src/Craig.js @@ -45,6 +45,8 @@ import { Overview } from "./components/pages/diagrams/Overview.js"; import PowerDiagram from "./components/pages/power/Power.js"; import VpcConnectivityPage from "./components/pages/vpc/Connectivity.js"; import ClassicDiagram from "./components/pages/classic/Classic.js"; +import * as htmlToImage from "html-to-image"; +import FileSaver from "file-saver"; const withRouter = (Page) => (props) => { const params = useParams(); @@ -91,6 +93,7 @@ class Craig extends React.Component { clickedWorkspace: "", clickedWorkspaceUrl: "", current_project: craig.store.project_name, + showOverviewForDownload: false, }; } catch (err) { // if there are initialization errors, redirect user to reset state path @@ -111,6 +114,7 @@ class Craig extends React.Component { this.getProject = this.getProject.bind(this); this.projectFetch = this.projectFetch.bind(this); this.saveProject = this.saveProject.bind(this); + this.showAndSnapshot = this.showAndSnapshot.bind(this); } // when react component mounts, set update callback for store @@ -125,6 +129,19 @@ class Craig extends React.Component { }); } + showAndSnapshot(callback) { + this.setState({ showOverviewForDownload: true }, () => { + htmlToImage + .toBlob(document.getElementById("overview-big")) + .then(function (blob) { + callback(blob); + }) + .then(() => { + this.setState({ showOverviewForDownload: false }); + }); + }); + } + /** * update components * @param {string} message @@ -569,6 +586,39 @@ class Craig extends React.Component { ) : ( <> + {this.state.showOverviewForDownload ? ( + // this is only shown when getting an image for the screengrab, + // otherwise will not be rendered + // the first div is a container for the overview image + // the second div is there to mask the first while creting the image + <> +
+ +
+
+ + ) : ( + "" + )} {this.props.params.doc ? ( this.props.params.doc === "about" ? ( diff --git a/client/src/app.scss b/client/src/app.scss index 7c8ad67d..5464bd43 100644 --- a/client/src/app.scss +++ b/client/src/app.scss @@ -397,6 +397,7 @@ input:read-only { position: relative; font-size: 80%; top: 20px; + left: -62px; } .cds--popover--open .cds--popover-content { @@ -1037,10 +1038,12 @@ div.banner-text { } .footerPopoverDismiss { + left: -143px !important; // important to override auto align right: 450% !important; // important to override auto align } .footerPopoverShow { + left: -130px !important; // important to override auto align right: 412.5% !important; // important to override auto align } @@ -2095,6 +2098,7 @@ input:read-only { position: relative; font-size: 80%; top: 20px; + left: -62px; } .cds--popover--open .cds--popover-content { diff --git a/client/src/components/forms/DynamicForm.js b/client/src/components/forms/DynamicForm.js index fb2c379e..ab204220 100644 --- a/client/src/components/forms/DynamicForm.js +++ b/client/src/components/forms/DynamicForm.js @@ -6,17 +6,15 @@ import { isFunction, getObjectFromArray, } from "lazy-z"; -import { forceShowForm, propsMatchState } from "../../lib"; import { + forceShowForm, + propsMatchState, dynamicCraigFormGroupsProps, dynamicHeadingProps, dynamicToolTipWrapperProps, -} from "../../lib/forms/dynamic-form-fields"; +} from "../../lib"; import { edgeRouterEnabledZones } from "../../lib/constants"; -import { - DynamicFetchMultiSelect, - DynamicFetchSelect, -} from "./dynamic-form/components"; +import { DynamicFetchMultiSelect, DynamicFetchSelect } from "./dynamic-form"; import { SubnetTileSubForm, SubnetTileTitle, @@ -266,9 +264,11 @@ class DynamicForm extends React.Component {
{this.props.form.groups.map((group, index) => - group.hideWhen && group.hideWhen(this.state) ? ( + group.hideWhen && group.hideWhen(this.state, this.props) ? ( "" - ) : group.heading && group.hideWhen && group.hideWhen(this.state) ? ( + ) : group.heading && + group.hideWhen && + group.hideWhen(this.state, this.props) ? ( "" ) : group.heading ? ( diff --git a/client/src/components/forms/dynamic-form/DynamicDatePicker.js b/client/src/components/forms/dynamic-form/DynamicDatePicker.js new file mode 100644 index 00000000..eb66f3ea --- /dev/null +++ b/client/src/components/forms/dynamic-form/DynamicDatePicker.js @@ -0,0 +1,41 @@ +import React from "react"; +import { DatePicker, DatePickerInput } from "@carbon/react"; +import PropTypes from "prop-types"; + +const DynamicDatePicker = (props) => { + // only used in opaque secrets, if we use this in other places we can + // change it to be more dynamic + return ( + { + let event = { + target: { + name: "expiration_date", + value: selectEvent[0], + }, + }; + props.handleInputChange(event); + }} + > + + + ); +}; + +DynamicDatePicker.propTypes = { + parentState: PropTypes.shape({ + expiration_date: PropTypes.string, + }).isRequired, + handleInputChange: PropTypes.func.isRequired, +}; + +export { DynamicDatePicker }; diff --git a/client/src/components/forms/dynamic-form/DynamicFetchMultiSelect.js b/client/src/components/forms/dynamic-form/DynamicFetchMultiSelect.js new file mode 100644 index 00000000..78a99731 --- /dev/null +++ b/client/src/components/forms/dynamic-form/DynamicFetchMultiSelect.js @@ -0,0 +1,74 @@ +import React from "react"; +import { FilterableMultiSelect } from "@carbon/react"; +import { dynamicMultiSelectProps } from "../../../lib"; +import PropTypes from "prop-types"; +import { deepEqual } from "lazy-z"; + +class DynamicFetchMultiSelect extends React.Component { + _isMounted = false; + constructor(props) { + super(props); + this.state = { + data: ["Loading..."], + }; + } + + componentDidMount() { + this._isMounted = true; + // on mount if not items have been set + if (deepEqual(this.state.data, ["Loading..."])) { + fetch( + // generate api endpoint based on state and props + this.props.field.apiEndpoint( + this.props.parentState, + this.props.parentProps + ) + ) + .then((res) => res.json()) + .then((data) => { + // set state with data if mounted + if (this._isMounted) { + this.setState({ data: data }, () => { + this.props.onPowerImageLoad(data); + }); + } + }) + .catch((err) => { + console.error(err); + }); + } + } + + componentWillUnmount() { + this._isMounted = false; + } + + // Force re-fetch of images on zone change + componentDidUpdate(prevProps) { + if (prevProps.parentState.zone != this.props.parentState.zone) { + this._isMounted = false; + this.setState({ data: ["Loading..."] }, () => { + this.componentDidMount(); + }); + } + } + + render() { + let props = { ...this.props }; + return ( + + ); + } +} + +DynamicFetchMultiSelect.propTypes = { + onPowerImageLoad: PropTypes.func.isRequired, + field: PropTypes.shape({ + apiEndpoint: PropTypes.func.isRequired, + }).isRequired, + parentState: PropTypes.shape({}), +}; + +export default DynamicFetchMultiSelect; diff --git a/client/src/components/forms/dynamic-form/DynamicFetchSelect.js b/client/src/components/forms/dynamic-form/DynamicFetchSelect.js new file mode 100644 index 00000000..37154ffe --- /dev/null +++ b/client/src/components/forms/dynamic-form/DynamicFetchSelect.js @@ -0,0 +1,135 @@ +import React from "react"; +import PropTypes from "prop-types"; +// popover wrapper needs to be imported this way to prevent an error importing +// dynamic form before initializtion +import { default as PopoverWrapper } from "../utils/PopoverWrapper"; +import { contains, deepEqual, isFunction, isNullOrEmptyString } from "lazy-z"; +import { dynamicFieldId, dynamicSelectProps } from "../../../lib"; +import { Select, SelectItem } from "@carbon/react"; + +class DynamicFetchSelect extends React.Component { + _isMounted = false; + constructor(props) { + super(props); + this.state = { + data: ["Loading..."], + }; + this.dataToGroups = this.dataToGroups.bind(this); + } + + componentDidMount() { + this._isMounted = true; + // on mount if not items have been set + if (deepEqual(this.state.data, ["Loading..."])) { + fetch( + // generate api endpoint based on state and props + this.props.field.apiEndpoint( + this.props.parentState, + this.props.parentProps + ) + ) + .then((res) => res.json()) + .then((data) => { + // set state with data if mounted + if (this._isMounted) { + this.setState({ data: data }); + } + }) + .catch((err) => { + console.error(err); + }); + } + } + + componentWillUnmount() { + this._isMounted = false; + } + + dataToGroups() { + let apiEndpoint = this.props.field.apiEndpoint( + this.props.parentState, + this.props.parentProps + ); + if (apiEndpoint === "/api/cluster/versions") { + // add "" if kube version is reset + return ( + this.props.parentProps.isModal || + isNullOrEmptyString(this.props.parentState.kube_version) + ? [""] + : [] + ).concat( + // filter version based on kube type + this.state.data.filter((version) => { + if ( + (this.props.parentState.kube_type === "openshift" && + contains(version, "openshift")) || + (this.props.parentState.kube_type === "iks" && + !contains(version, "openshift")) || + version === "default" + ) { + return version.replace(/\s\(Default\)/g, ""); + } + }) + ); + } else { + return ( + // to prevent storage pools from being loaded incorrectly, + // prevent first item in storage groups from being loaded when not selected + ( + dynamicSelectProps(this.props).value === "" && + this._isMounted && + !deepEqual(this.state.data, ["Loading..."]) + ? [""] + : [] + ) + .concat(this.state.data) + .map((item) => { + if (isFunction(this.props.field.onRender)) { + return this.props.field.onRender({ + [this.props.name]: item, + }); + } else return item; + }) + ); + } + } + + render() { + let props = { ...this.props }; + return ( + + + + ); + } +} + +DynamicFetchSelect.propTypes = { + parentState: PropTypes.shape({ + kube_version: PropTypes.string, + }).isRequired, + parentProps: PropTypes.shape({ + isModal: PropTypes.bool, + }).isRequired, + field: PropTypes.shape({ + apiEndpoint: PropTypes.func.isRequired, + onRender: PropTypes.func, + tooltip: PropTypes.shape({}), + }).isRequired, +}; + +export default DynamicFetchSelect; diff --git a/client/src/components/forms/dynamic-form/PowerInterfaces.js b/client/src/components/forms/dynamic-form/PowerInterfaces.js index 26d53f85..b025b9b1 100644 --- a/client/src/components/forms/dynamic-form/PowerInterfaces.js +++ b/client/src/components/forms/dynamic-form/PowerInterfaces.js @@ -2,11 +2,35 @@ import React from "react"; import PropTypes from "prop-types"; import { Network_3 } from "@carbon/icons-react"; import { DynamicFormTextInput } from "./components"; -import { contains } from "lazy-z"; +import { contains, isEmpty, isNullOrEmptyString } from "lazy-z"; import { CraigFormGroup } from "../utils"; +import { CraigEmptyResourceTile } from "./tiles"; export const PowerInterfaces = (props) => { - return contains(["Power Instances", "VTL"], props.componentProps.formName) ? ( + let isVtl = contains(["VTL"], props.componentProps.formName); + return isVtl && + (!props.stateData.workspace || + isEmpty( + props.componentProps.craig.vtl.image.groups( + props.stateData, + props.componentProps + ) + )) ? ( + + VTL images selected in Power VS workspace{" "} + {props.stateData.workspace}. + Add images from the Power VS form + + ) + } + /> + ) : contains(["Power Instances", "VTL"], props.componentProps.formName) ? (
{props.stateData.network.map((nw, index) => { return ( diff --git a/client/src/components/forms/dynamic-form/components.js b/client/src/components/forms/dynamic-form/components.js index 293e3b4b..ec979aad 100644 --- a/client/src/components/forms/dynamic-form/components.js +++ b/client/src/components/forms/dynamic-form/components.js @@ -9,7 +9,8 @@ import { dynamicToggleProps, dynamicTextAreaProps, dynamicMultiSelectProps, -} from "../../../lib/forms/dynamic-form-fields"; + dynamicPasswordInputProps, +} from "../../../lib"; import { FilterableMultiSelect, SelectItem, @@ -18,12 +19,8 @@ import { TextInput, Select, Tag, - DatePicker, - DatePickerInput, } from "@carbon/react"; import PropTypes from "prop-types"; -import { dynamicPasswordInputProps } from "../../../lib/forms/dynamic-form-fields/password-input"; -import { contains, deepEqual, isFunction, isNullOrEmptyString } from "lazy-z"; import { ToolTipWrapper } from "../utils/ToolTip"; import { RenderForm } from "../utils"; @@ -224,204 +221,6 @@ DynamicPublicKey.propTypes = { handleInputChange: PropTypes.func.isRequired, }; -export class DynamicFetchSelect extends React.Component { - _isMounted = false; - constructor(props) { - super(props); - this.state = { - data: ["Loading..."], - }; - this.dataToGroups = this.dataToGroups.bind(this); - } - - componentDidMount() { - this._isMounted = true; - // on mount if not items have been set - if (deepEqual(this.state.data, ["Loading..."])) - fetch( - // generate api endpoint based on state and props - this.props.field.apiEndpoint( - this.props.parentState, - this.props.parentProps - ) - ) - .then((res) => res.json()) - .then((data) => { - // set state with data if mounted - if (this._isMounted) { - this.setState({ data: data }); - } - }) - .catch((err) => { - console.error(err); - }); - } - - componentWillUnmount() { - this._isMounted = false; - } - - dataToGroups() { - let apiEndpoint = this.props.field.apiEndpoint( - this.props.parentState, - this.props.parentProps - ); - if (apiEndpoint === "/api/cluster/versions") { - // add "" if kube version is reset - return ( - this.props.parentProps.isModal || - isNullOrEmptyString(this.props.parentState.kube_version) - ? [""] - : [] - ).concat( - // filter version based on kube type - this.state.data.filter((version) => { - if ( - (this.props.parentState.kube_type === "openshift" && - contains(version, "openshift")) || - (this.props.parentState.kube_type === "iks" && - !contains(version, "openshift")) || - version === "default" - ) { - return version.replace(/\s\(Default\)/g, ""); - } - }) - ); - } else { - return ( - // to prevent storage pools from being loaded incorrectly, - // prevent first item in storage groups from being loaded when not selected - ( - dynamicSelectProps(this.props).value === "" && - this._isMounted && - !deepEqual(this.state.data, ["Loading..."]) - ? [""] - : [] - ) - .concat(this.state.data) - .map((item) => { - if (isFunction(this.props.field.onRender)) { - return this.props.field.onRender({ - [this.props.name]: item, - }); - } else return item; - }) - ); - } - } - - render() { - let props = { ...this.props }; - return ( - - - - ); - } -} - -export class DynamicFetchMultiSelect extends React.Component { - _isMounted = false; - constructor(props) { - super(props); - this.state = { - data: ["Loading..."], - }; - } - - componentDidMount() { - this._isMounted = true; - // on mount if not items have been set - if (deepEqual(this.state.data, ["Loading..."])) { - fetch( - // generate api endpoint based on state and props - this.props.field.apiEndpoint( - this.props.parentState, - this.props.parentProps - ) - ) - .then((res) => res.json()) - .then((data) => { - // set state with data if mounted - if (this._isMounted) { - this.setState({ data: data }, () => { - this.props.onPowerImageLoad(data); - }); - } - }) - .catch((err) => { - console.error(err); - }); - } - } - - componentWillUnmount() { - this._isMounted = false; - } - - // Force re-fetch of images on zone change - componentDidUpdate(prevProps) { - if (prevProps.parentState.zone != this.props.parentState.zone) { - this._isMounted = false; - this.setState({ data: ["Loading..."] }, () => { - this.componentDidMount(); - }); - } - } - - render() { - let props = { ...this.props }; - return ( - - ); - } -} - -const DynamicDatePicker = (props) => { - // only used in opaque secrets, if we use this in other places we can - // change it to be more dynamic - return ( - { - let event = { - target: { - name: "expiration_date", - value: selectEvent[0], - }, - }; - props.handleInputChange(event); - }} - > - - - ); -}; - export { DynamicFormTextInput, DynamicFormSelect, @@ -431,5 +230,4 @@ export { DynamicPublicKey, DynamicToolTipWrapper, tagColors, - DynamicDatePicker, }; diff --git a/client/src/components/forms/dynamic-form/index.js b/client/src/components/forms/dynamic-form/index.js index 9b26bbb1..095d61ba 100644 --- a/client/src/components/forms/dynamic-form/index.js +++ b/client/src/components/forms/dynamic-form/index.js @@ -5,11 +5,13 @@ export { DynamicFormToggle, DynamicTextArea, DynamicPublicKey, - DynamicDatePicker, DynamicToolTipWrapper, } from "./components"; +export { DynamicDatePicker } from "./DynamicDatePicker"; export { PowerInterfaces } from "./PowerInterfaces"; export { SubFormOverrideTile } from "./SubFormOverrideTile"; +export { default as DynamicFetchMultiSelect } from "./DynamicFetchMultiSelect"; +export { default as DynamicFetchSelect } from "./DynamicFetchSelect"; export { ClassicDisabledTile, NoClassicGatewaysTile, diff --git a/client/src/components/forms/utils/PrimaryButton.js b/client/src/components/forms/utils/PrimaryButton.js index 743e5d15..ee122a36 100644 --- a/client/src/components/forms/utils/PrimaryButton.js +++ b/client/src/components/forms/utils/PrimaryButton.js @@ -57,7 +57,7 @@ PrimaryButton.defaultProps = { hoverText: "Save Changes", inline: false, disabled: false, - hoverTextAlign: "bottom", + hoverTextAlign: "bottom-right", }; PrimaryButton.propTypes = { diff --git a/client/src/components/forms/utils/SecondaryButton.js b/client/src/components/forms/utils/SecondaryButton.js index de70618d..0cad3ca7 100644 --- a/client/src/components/forms/utils/SecondaryButton.js +++ b/client/src/components/forms/utils/SecondaryButton.js @@ -1,12 +1,16 @@ import React from "react"; import PropTypes from "prop-types"; +import { contains } from "lazy-z"; import { default as PopoverWrapper } from "./PopoverWrapper"; import { TrashCan } from "@carbon/icons-react"; import { Button } from "@carbon/react"; import { dynamicSecondaryButtonProps } from "../../../lib/components/toggle-form-components"; export const SecondaryButton = (props) => { - let buttonProps = dynamicSecondaryButtonProps(props); + let isV2Page = + contains(window.location.pathname, "/v2") || + contains(window.location.search, "v2"); + let buttonProps = dynamicSecondaryButtonProps(props, isV2Page); return (
{ SecondaryButton.defaultProps = { disabled: false, - hoverTextAlign: "bottom", + hoverTextAlign: "bottom-right", }; SecondaryButton.propTypes = { diff --git a/client/src/components/forms/utils/ToggleFormComponents.js b/client/src/components/forms/utils/ToggleFormComponents.js index 647cb798..a8d3be7c 100644 --- a/client/src/components/forms/utils/ToggleFormComponents.js +++ b/client/src/components/forms/utils/ToggleFormComponents.js @@ -13,7 +13,7 @@ import { dynamicSecondaryButtonProps, statelessWrapperProps, } from "../../../lib/components/toggle-form-components"; -import { kebabCase } from "lazy-z"; +import { contains, kebabCase } from "lazy-z"; import { DynamicToolTipWrapper } from "../dynamic-form/components"; import PopoverWrapper from "./PopoverWrapper"; @@ -100,7 +100,7 @@ PrimaryButton.defaultProps = { hoverText: "Save Changes", inline: false, disabled: false, - hoverTextAlign: "bottom", + hoverTextAlign: "bottom-right", }; PrimaryButton.propTypes = { @@ -114,7 +114,10 @@ PrimaryButton.propTypes = { }; const SecondaryButton = (props) => { - let buttonProps = dynamicSecondaryButtonProps(props); + let isV2Page = + contains(window.location.pathname, "/v2") || + contains(window.location.search, "v2"); + let buttonProps = dynamicSecondaryButtonProps(props, isV2Page); return (
{ SecondaryButton.defaultProps = { disabled: false, - hoverTextAlign: "bottom", + hoverTextAlign: "bottom-right", }; SecondaryButton.propTypes = { diff --git a/client/src/components/page-template/Navigation.js b/client/src/components/page-template/Navigation.js index 101d07de..0a20f86b 100644 --- a/client/src/components/page-template/Navigation.js +++ b/client/src/components/page-template/Navigation.js @@ -89,17 +89,22 @@ class Navigation extends React.Component { }; } if (validated) { - let error = downloadContent(this.props.json, this.props.project?.name); - if (error) { - console.error(error); - notification = { - title: "Error", - kind: "error", - text: `Unable to download configuration.\n${error.message}`, - }; - } + this.props.showAndSnapshot((imageBlob) => { + let error = downloadContent( + this.props.json, + this.props.project?.name, + imageBlob + ); + if (error) { + console.error(error); + notification = { + title: "Error", + kind: "error", + text: `Unable to download configuration.\n${error.message}`, + }; + } + }); } - this.props.notify(notification); } resetSearch() { diff --git a/client/src/components/page-template/PageTemplate.js b/client/src/components/page-template/PageTemplate.js index 18aa8669..f53f1fa5 100644 --- a/client/src/components/page-template/PageTemplate.js +++ b/client/src/components/page-template/PageTemplate.js @@ -45,6 +45,8 @@ import { AppConnectivity, ChartLine, SecurityServices, + InstanceClassic, + IbmCloudBareMetalServer, } from "@carbon/icons-react"; import f5 from "../../images/f5.png"; import { @@ -112,6 +114,8 @@ const navIcons = { LoadBalancerPool: LoadBalancerPool, AppConnectivity: AppConnectivity, SecurityServices: SecurityServices, + InstanceClassic: InstanceClassic, + IbmCloudBareMetalServer: IbmCloudBareMetalServer, }; let pageOrder = [ @@ -249,6 +253,7 @@ const PageTemplate = (props) => { isResetState={isResetState} formPathNotPresent={formPathNotPresent} invalidForms={props.invalidForms} + showAndSnapshot={props.showAndSnapshot} /> {!isResetState && ( <> diff --git a/client/src/components/pages/CraigForms.js b/client/src/components/pages/CraigForms.js index f02b8b70..cc97fd03 100644 --- a/client/src/components/pages/CraigForms.js +++ b/client/src/components/pages/CraigForms.js @@ -1,4 +1,4 @@ -const { contains } = require("lazy-z"); +const { contains, isNullOrEmptyString, isEmpty } = require("lazy-z"); const { edgeRouterEnabledZones } = require("../../lib/constants"); const { disableSave } = require("../../lib"); @@ -387,6 +387,34 @@ function craigForms(craig) { }, ], }, + classic_bare_metal: { + jsonField: "classic_bare_metal", + groups: [ + { + name: craig.classic_bare_metal.name, + domain: craig.classic_bare_metal.domain, + datacenter: craig.classic_bare_metal.datacenter, + }, + { + os_key_name: craig.classic_bare_metal.os_key_name, + package_key_name: craig.classic_bare_metal.package_key_name, + process_key_name: craig.classic_bare_metal.process_key_name, + }, + { + memory: craig.classic_bare_metal.memory, + network_speed: craig.classic_bare_metal.network_speed, + private_network_only: craig.classic_bare_metal.private_network_only, + }, + { + private_vlan: craig.classic_bare_metal.private_vlan, + public_vlan: craig.classic_bare_metal.public_vlan, + public_bandwidth: craig.classic_bare_metal.public_bandwidth, + }, + { + disk_key_names: craig.classic_bare_metal.disk_key_names, + }, + ], + }, classic_security_groups: { jsonField: "classic_security_groups", groups: [ @@ -398,6 +426,35 @@ function craigForms(craig) { }, ], }, + classic_vsi: { + jsonField: "classic_vsi", + groups: [ + { + name: craig.classic_vsi.name, + domain: craig.classic_vsi.domain, + datacenter: craig.classic_vsi.datacenter, + }, + { + cores: craig.classic_vsi.cores, + memory: craig.classic_vsi.memory, + image_id: craig.classic_vsi.image_id, + }, + { + network_speed: craig.classic_vsi.network_speed, + local_disk: craig.classic_vsi.local_disk, + ssh_keys: craig.classic_vsi.ssh_keys, + }, + { + private_vlan: craig.classic_vsi.private_vlan, + private_security_groups: craig.classic_vsi.private_security_groups, + private_network_only: craig.classic_vsi.private_network_only, + }, + { + public_vlan: craig.classic_vsi.public_vlan, + public_security_groups: craig.classic_vsi.public_security_groups, + }, + ], + }, classic_gateways: { jsonField: "classic_gateways", groups: [ @@ -1018,25 +1075,24 @@ function craigForms(craig) { { name: craig.power_instances.name, workspace: craig.power_instances.workspace, + ssh_key: craig.power_instances.ssh_key, }, { network: craig.power_instances.network, primary_subnet: craig.power_instances.primary_subnet, + pi_pin_policy: craig.power_instances.pi_pin_policy, }, { - ssh_key: craig.power_instances.ssh_key, image: craig.power_instances.image, pi_sys_type: craig.power_instances.pi_sys_type, + pi_storage_pool_affinity: + craig.power_instances.pi_storage_pool_affinity, }, { pi_proc_type: craig.power_instances.pi_proc_type, pi_processors: craig.power_instances.pi_processors, pi_memory: craig.power_instances.pi_memory, }, - { - pi_storage_pool_affinity: - craig.power_instances.pi_storage_pool_affinity, - }, { pi_ibmi_css: craig.power_instances.pi_ibmi_css, pi_ibmi_pha: craig.power_instances.pi_ibmi_pha, @@ -1047,6 +1103,9 @@ function craigForms(craig) { name: "Boot Volume", type: "subHeading", }, + hideWhen: function (stateData) { + return isNullOrEmptyString(stateData.workspace, true); + }, }, { pi_storage_type: craig.power_instances.pi_storage_type, @@ -1070,6 +1129,9 @@ function craigForms(craig) { name: "IP Interface Options", type: "subHeading", }, + hideWhen: function (stateData) { + return isNullOrEmptyString(stateData.workspace, true); + }, }, ], }, @@ -1572,6 +1634,12 @@ function craigForms(craig) { name: "Boot Volume", type: "subHeading", }, + hideWhen: function (stateData, componentProps) { + return ( + isNullOrEmptyString(stateData.workspace, true) || + isEmpty(craig.vtl.image.groups(stateData, componentProps)) + ); + }, }, { storage_option: craig.vtl.storage_option, @@ -1588,6 +1656,12 @@ function craigForms(craig) { name: "IP Interface Options", type: "subHeading", }, + hideWhen: function (stateData, componentProps) { + return ( + isNullOrEmptyString(stateData.workspace, true) || + isEmpty(craig.vtl.image.groups(stateData, componentProps)) + ); + }, }, ], }, diff --git a/client/src/components/pages/FormPages.js b/client/src/components/pages/FormPages.js index 0d6bb8ee..a2ba218e 100644 --- a/client/src/components/pages/FormPages.js +++ b/client/src/components/pages/FormPages.js @@ -262,6 +262,15 @@ const CisGlbs = (craig) => { }); }; +const ClassicBareMetal = (craig) => { + return formPageTemplate(craig, { + name: "Classic Bare Metal Servers", + addText: "Create a Bare Metal Server", + formName: "classic-bare-metal", + jsonField: "classic_bare_metal", + }); +}; + const ClassicSecurityGroups = (craig) => { return formPageTemplate(craig, { name: "Classic Security Groups", @@ -271,6 +280,15 @@ const ClassicSecurityGroups = (craig) => { }); }; +const ClassicVsi = (craig) => { + return formPageTemplate(craig, { + name: "Classic Virtual Servers", + addText: "Create a Virtual Server", + formName: "classic-vsi", + jsonField: "classic_vsi", + }); +}; + const ClassicGateways = (craig) => { return formPageTemplate(craig, { name: "Classic Gateways", @@ -1339,8 +1357,12 @@ export const NewFormPage = (props) => { return Cis(craig); } else if (form === "cisGlbs") { return CisGlbs(craig); + } else if (form === "classicBareMetal") { + return ClassicBareMetal(craig); } else if (form === "classicSecurityGroups") { return ClassicSecurityGroups(craig); + } else if (form === "classicVsi") { + return ClassicVsi(craig); } else if (form === "classicGateways") { return ClassicGateways(craig); } else if (form === "classicSshKeys") { diff --git a/client/src/components/pages/Home.js b/client/src/components/pages/Home.js index e7320e5c..dc643e28 100644 --- a/client/src/components/pages/Home.js +++ b/client/src/components/pages/Home.js @@ -87,10 +87,21 @@ function Home(props) { endpoints: craig.options.endpoints, account_id: craig.options.account_id, }, + { + heading: { + name: "Power VS", + type: "subHeading", + className: "marginBottomSmall", + }, + }, { enable_power_vs: craig.options.enable_power_vs, power_vs_high_availability: craig.options.power_vs_high_availability, + }, + { + power_vs_ha_zone_1: craig.options.power_vs_ha_zone_1, + power_vs_ha_zone_2: craig.options.power_vs_ha_zone_2, power_vs_zones: craig.options.power_vs_zones, }, { diff --git a/client/src/components/pages/classic/Classic.js b/client/src/components/pages/classic/Classic.js index 4b47ed88..40bddb74 100644 --- a/client/src/components/pages/classic/Classic.js +++ b/client/src/components/pages/classic/Classic.js @@ -4,6 +4,8 @@ import { ClassicMap, ClassicSecurityGroups, ClassicSubnets, + ClassicBareMetal, + ClassicVsi, SshKeys, docTabs, } from "../diagrams"; @@ -14,6 +16,8 @@ import { Password, SecurityServices, VlanIbm, + IbmCloudBareMetalServer, + InstanceClassic, } from "@carbon/icons-react"; import { StatefulTabs, @@ -26,6 +30,8 @@ import { import { classicInfraTf, classicSecurityGroupTf, + classicBareMetalTf, + classicVsiTf, disableSave, propsMatchState, } from "../../../lib"; @@ -51,6 +57,8 @@ class ClassicDiagram extends React.Component { this.resetSelection = this.resetSelection.bind(this); this.onVlanClick = this.onVlanClick.bind(this); this.onSgClick = this.onSgClick.bind(this); + this.onBareMetalClick = this.onBareMetalClick.bind(this); + this.onVsiClick = this.onVsiClick.bind(this); this.getIcon = this.getIcon.bind(this); this.onGwClick = this.onGwClick.bind(this); this.handleInputChange = this.handleInputChange.bind(this); @@ -90,6 +98,10 @@ class ClassicDiagram extends React.Component { ? Password : this.state.selectedItem === "classic_vlans" ? VlanIbm + : this.state.selectedItem === "classic_bare_metal" + ? IbmCloudBareMetalServer + : this.state.selectedItem === "classic_vsi" + ? InstanceClassic : FirewallClassic; } @@ -166,6 +178,46 @@ class ClassicDiagram extends React.Component { } } + /** + * on bare metal click + * @param {number} bareMetalIndex + */ + onBareMetalClick(bareMetalIndex) { + if ( + this.state.editing && + this.state.selectedItem === "classic_bare_metal" && + bareMetalIndex === this.state.selectedIndex + ) { + this.resetSelection(); + } else { + this.setState({ + editing: true, + selectedIndex: bareMetalIndex, + selectedItem: "classic_bare_metal", + }); + } + } + + /** + * on classic vsi click + * @param {number} vsiIndex + */ + onVsiClick(vsiIndex) { + if ( + this.state.editing && + this.state.selectedItem === "classic_vsi" && + vsiIndex === this.state.selectedIndex + ) { + this.resetSelection(); + } else { + this.setState({ + editing: true, + selectedIndex: vsiIndex, + selectedItem: "classic_vsi", + }); + } + } + /** * on ssh key click * @param {number} keyIndex @@ -228,6 +280,8 @@ class ClassicDiagram extends React.Component { "Classic VLANs", "Classic Gateways", "Classic Security Groups", + "Classic Bare Metal", + "Classic VSI", ], disabled: (stateData) => { return false; @@ -257,6 +311,7 @@ class ClassicDiagram extends React.Component { name={`New ${titleCase(this.state.modalService) .replace("Vlans", "VLANs") .replace("Ssh", "SSH") + .replace("Vsi", "VSI") .replace(/s$/g, "")}`} className="marginBottomSmall" type="subHeading" @@ -317,6 +372,8 @@ class ClassicDiagram extends React.Component { "Classic VLANs", "Classic Gateways", "Classic Security Groups", + "Classic Bare Metal", + "Classic VSIs", ], craig )} @@ -333,6 +390,14 @@ class ClassicDiagram extends React.Component { name: "Classic Security Groups", tf: classicSecurityGroupTf(craig.store.json) || "", }, + { + name: "Classic Bare Metal", + tf: classicBareMetalTf(craig.store.json) || "", + }, + { + name: "Classic VSIs", + tf: classicVsiTf(craig.store.json) || "", + }, ]} form={ <> @@ -418,6 +483,27 @@ class ClassicDiagram extends React.Component { ); }} /> + { + return ( + this.state.selectedItem === "classic_vsi" && + this.state.selectedIndex === vsiIndex + ); + }} + onClick={this.onVsiClick} + /> + { + return ( + this.state.selectedItem === + "classic_bare_metal" && + this.state.selectedIndex === bareMetalIndex + ); + }} + onClick={this.onBareMetalClick} + />
@@ -447,6 +533,11 @@ class ClassicDiagram extends React.Component { ? "Classic SSH Key" : this.state.selectedItem === "classic_vlans" ? "VLAN" + : this.state.selectedItem === + "classic_bare_metal" + ? "Classic Bare Metal" + : this.state.selectedItem === "classic_vsi" + ? "Classic VSI" : "Classic Gateway" } ${ craig.store.json[this.state.selectedItem][ diff --git a/client/src/components/pages/cloud-services/CloudServices.js b/client/src/components/pages/cloud-services/CloudServices.js index 916371ad..81596fdf 100644 --- a/client/src/components/pages/cloud-services/CloudServices.js +++ b/client/src/components/pages/cloud-services/CloudServices.js @@ -424,10 +424,12 @@ class CloudServicesPage extends React.Component { ? "Activity Tracker" : this.state.modalService === "scc_v2" ? "Security & Compliance Center" + : this.state.modalService === "dns" + ? "DNS" : titleCase(this.state.modalService) ) .replace( - contains(["dns", "event_streams"]) ? "" : /s(?=$)/g, + contains(["DNS", "event_streams"]) ? "" : /s(?=$)/g, "" ) .replace("Dns", "DNS")}${ diff --git a/client/src/components/pages/diagrams/ClassicBareMetal.js b/client/src/components/pages/diagrams/ClassicBareMetal.js new file mode 100644 index 00000000..e7732b94 --- /dev/null +++ b/client/src/components/pages/diagrams/ClassicBareMetal.js @@ -0,0 +1,69 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { PowerSubnetInnerBox } from "./PowerSubnetInnerBox"; +import { IbmCloudBareMetalServer } from "@carbon/icons-react"; +import { DeploymentIcon } from "./DeploymentIcon"; +import HoverClassNameWrapper from "./HoverClassNameWrapper"; + +export const ClassicBareMetal = (props) => { + let bare_metals = []; + props.craig.store.json.classic_bare_metal.forEach((server, serverIndex) => { + if ( + server.private_vlan === props.vlan || + server.public_vlan === props.vlan + ) { + let copyServer = { ...server }; + copyServer.index = serverIndex; + bare_metals.push(copyServer); + } + }); + return bare_metals.length === 0 ? ( + "" + ) : ( + + {bare_metals.map((server) => { + return ( + + { + props.onClick(server.index); + } + : undefined + } + isSelected={ + props.isSelected + ? () => props.isSelected(server.index) + : undefined + } + /> + + ); + })} + + ); +}; + +ClassicBareMetal.propTypes = { + craig: PropTypes.shape({}).isRequired, + isSelected: PropTypes.func, + onClick: PropTypes.func, + vlan: PropTypes.string, + datacenter: PropTypes.string, +}; diff --git a/client/src/components/pages/diagrams/ClassicGateways.js b/client/src/components/pages/diagrams/ClassicGateways.js index ec789e0e..af8753d4 100644 --- a/client/src/components/pages/diagrams/ClassicGateways.js +++ b/client/src/components/pages/diagrams/ClassicGateways.js @@ -30,6 +30,7 @@ export const ClassicGateways = (props) => { name="Classic Gateways" static={props.static} small={props.small} + marginBottom={props.craig.store.json.classic_vsi.length !== 0} > {gateways.map((gw) => { return ( diff --git a/client/src/components/pages/diagrams/ClassicSecurityGroups.js b/client/src/components/pages/diagrams/ClassicSecurityGroups.js index 70781bb5..ab8b91e4 100644 --- a/client/src/components/pages/diagrams/ClassicSecurityGroups.js +++ b/client/src/components/pages/diagrams/ClassicSecurityGroups.js @@ -11,7 +11,7 @@ export const ClassicSecurityGroups = (props) => { return ( { + let vsis = []; + props.craig.store.json.classic_vsi.forEach((vsi, vsiIndex) => { + if (vsi.private_vlan === props.vlan || vsi.public_vlan === props.vlan) { + let copyVsi = { ...vsi }; + copyVsi.index = vsiIndex; + vsis.push(copyVsi); + } + }); + return vsis.length === 0 ? ( + "" + ) : ( + + {vsis.map((vsi) => { + return ( + + { + props.onClick(vsi.index); + } + : undefined + } + isSelected={ + props.isSelected ? () => props.isSelected(vsi.index) : undefined + } + /> + + ); + })} + + ); +}; + +ClassicVsi.propTypes = { + craig: PropTypes.shape({}).isRequired, + isSelected: PropTypes.func, + onClick: PropTypes.func, + vlan: PropTypes.string, + datacenter: PropTypes.string, +}; diff --git a/client/src/components/pages/diagrams/DocTabs.js b/client/src/components/pages/diagrams/DocTabs.js index 8243a375..7b3e4c00 100644 --- a/client/src/components/pages/diagrams/DocTabs.js +++ b/client/src/components/pages/diagrams/DocTabs.js @@ -36,6 +36,8 @@ export const docTabs = (tabs, craig) => { ? "subnets" : tab === "Virtual Servers" ? "vsi" + : tab === "Classic VSIs" + ? "classic_vsi" : tab ), craig.store.json._options.template diff --git a/client/src/components/pages/diagrams/Overview.js b/client/src/components/pages/diagrams/Overview.js index 4b27ae97..a044d690 100644 --- a/client/src/components/pages/diagrams/Overview.js +++ b/client/src/components/pages/diagrams/Overview.js @@ -26,6 +26,8 @@ import { ClassicSubnets } from "./ClassicSubnets"; import { ClassicGateways } from "./ClassicGateways"; import { RoutingTables } from "./RoutingTables"; import { PassThroughWrapper } from "./PassthroughWrapper"; +import { ClassicVsi } from "./ClassicVsi"; +import { ClassicBareMetal } from "./ClassicBareMetal"; export class Overview extends React.Component { constructor(props) { @@ -35,7 +37,7 @@ export class Overview extends React.Component { render() { let craig = this.props.craig; return ( -
+
{this.props.small ? ( "" ) : ( @@ -51,175 +53,194 @@ export class Overview extends React.Component { /> )}
-
- } - /> -
- -
-
-
-
- } - /> - {this.props.small || craig.store.json.ssh_keys.length === 0 ? ( - "" - ) : ( -
- -
- )} -
- - {this.props.small ? ( - <> - ) : ( - - )} - {this.props.small ? ( - <> - ) : ( - - )} - - - } - small={this.props.small} - /> - - -
-
- {craig.store.json.power.length === 0 ? ( - "" - ) : (
} + icon={} /> - - - - - - {this.props.small ? ( - <> - ) : ( - - - - )} - - +
+ +
- )} - {craig.store.json._options.enable_classic ? (
} + icon={} /> -
- - - + +
+ )} +
+ + {this.props.small ? ( + <> + ) : ( + + )} + {this.props.small ? ( + <> + ) : ( + + )} + + + } small={this.props.small} /> - - + +
- ) : ( - "" - )} - {craig.store.json.transit_gateways.length === 0 || this.props.small ? ( - "" - ) : ( -
-
- } - /> -
- +
+ } /> + + + + + + {this.props.small ? ( + <> + ) : ( + + + + )} + +
-
- )} + )} + {craig.store.json._options.enable_classic ? ( +
+
+ } + /> +
+ + + + + + + +
+
+ ) : ( + "" + )} + {craig.store.json.transit_gateways.length === 0 || + this.props.small ? ( + "" + ) : ( +
+
+ } + /> +
+ +
+
+ )} +
); } diff --git a/client/src/components/pages/diagrams/PowerSubnetInnerBox.js b/client/src/components/pages/diagrams/PowerSubnetInnerBox.js index 992dda63..2aa813c7 100644 --- a/client/src/components/pages/diagrams/PowerSubnetInnerBox.js +++ b/client/src/components/pages/diagrams/PowerSubnetInnerBox.js @@ -9,6 +9,9 @@ export const PowerSubnetInnerBox = (props) => { if (props.marginTop) { powerInnerBoxClassName += " marginTopThreeQuartersRem"; } + if (props.marginBottom && !props.small) { + powerInnerBoxClassName += " marginBottomSmall"; + } return ( { let craig = props.craig; @@ -203,7 +204,9 @@ export const VpcMap = (props) => { : {} } hoverClassName={ - props.small ? "" : "diagramBoxSelected" + props.small || props.static + ? "" + : "diagramBoxSelected" } > { vpc={vpc} small={props.small} imported - /> + craig={craig} + vpcIndex={vpcIndex} + > + { + props.onImportedSubnetItemClick( + vpcIndex, + field, + itemIndex + ); + } + : undefined + } + parentState={props.parentState} + small={props.small} + static={props.static} + tabSelected={props.tabSelected} + onTabClick={props.onTabClick} + /> + ); })} diff --git a/client/src/components/pages/diagrams/index.js b/client/src/components/pages/diagrams/index.js index 20315d2c..b539963f 100644 --- a/client/src/components/pages/diagrams/index.js +++ b/client/src/components/pages/diagrams/index.js @@ -17,3 +17,5 @@ export { PowerSubnetInnerBox } from "./PowerSubnetInnerBox"; export { TransitGatewaysMap } from "./TransitGatewaysMap"; export { ScrollFormWrapper } from "./ScrollFormWrapper"; export { ClassicSecurityGroups } from "./ClassicSecurityGroups"; +export { ClassicBareMetal } from "./ClassicBareMetal.js"; +export { ClassicVsi } from "./ClassicVsi.js"; diff --git a/client/src/components/pages/power/Power.js b/client/src/components/pages/power/Power.js index 4a9013a8..b38569f4 100644 --- a/client/src/components/pages/power/Power.js +++ b/client/src/components/pages/power/Power.js @@ -306,7 +306,9 @@ class PowerDiagram extends React.Component { craig={craig} modalService={this.state.modalService} formName={ - this.state.modalService === "power_volumes" + this.state.modalService === "vtl" + ? "VTL" + : this.state.modalService === "power_volumes" ? undefined : "Power Instances" } @@ -441,6 +443,10 @@ class PowerDiagram extends React.Component { craig.options.power_vs_high_availability, }, { + power_vs_ha_zone_1: + craig.options.power_vs_ha_zone_1, + power_vs_ha_zone_2: + craig.options.power_vs_ha_zone_2, power_vs_zones: craig.options.power_vs_zones, }, ], diff --git a/client/src/components/pages/projects/JSONModal.js b/client/src/components/pages/projects/JSONModal.js index 23bed712..02ae36c1 100644 --- a/client/src/components/pages/projects/JSONModal.js +++ b/client/src/components/pages/projects/JSONModal.js @@ -20,9 +20,10 @@ export class JSONModal extends React.Component { error: "", }; - if (this.props.import) { + if (this.props.import && !this.props.uploadedJsonData) { this.state.name = ""; } else if (this.state.json) { + if (this.props.uploadedJsonData) this.state.name = ""; try { validate(this.state.json); this.state.isValid = true; @@ -82,7 +83,6 @@ export class JSONModal extends React.Component { this.state.json === undefined ? true : this.props.data && !deepEqual(this.props.data.json, this.state.json); - return ( { + let isUpload = props.action === "upload"; + let task = isUpload ? "Upload" : "Create"; + let isFailed = props.failed === true; return ( { open={props.open} passiveModal={!props.completed} modalHeading={ - props.action === "upload" - ? props.completed === false - ? `Upload to Schematics Workspace: ${props.workspace}` - : "Upload " + (props.failed === true ? "Failed!" : "Completed!") - : props.completed === false - ? `Create Schematics Workspace: ${props.workspace}` - : "Create " + (props.failed === true ? "Failed!" : "Completed!") + props.completed === false + ? `${task} Schematics Workspace: ${props.workspace}` + : `${task} ` + (isFailed ? "Failed!" : "Completed!") } // complete onRequestSubmit={() => { - if (props.failed === true) { + if (isFailed) { if (props.action === "create") { // for fake state for onProjectSave when action is "create" let fakeState = { @@ -50,16 +49,15 @@ export const LoadingModal = (props) => { } else props.retryCallback(); } else { // didn't fail so only need onRequestSubmit for upload - props.action === "upload" - ? window.open(props.workspace_url + "/jobs", "_blank") - : window.open(props.workspace_url, "_blank"); + window.open( + props.workspace_url + (isUpload ? "/jobs" : ""), + "_blank" + ); } }} onSecondarySubmit={props.toggleModal} - primaryButtonText={ - props.failed === true ? "Retry" : "Launch Workspace in New Tab" - } - secondaryButtonText={props.failed === true ? "Cancel" : "Close"} + primaryButtonText={isFailed ? "Retry" : "Launch Workspace in New Tab"} + secondaryButtonText={isFailed ? "Cancel" : "Close"} onRequestClose={props.toggleModal} > {props.completed === false ? ( @@ -73,7 +71,7 @@ export const LoadingModal = (props) => { ) : ( <> - {props.failed === true ? ( + {isFailed ? ( <> {/* when complete but failed show X */}
diff --git a/client/src/components/pages/projects/Projects.js b/client/src/components/pages/projects/Projects.js index 7535b493..099e24d6 100644 --- a/client/src/components/pages/projects/Projects.js +++ b/client/src/components/pages/projects/Projects.js @@ -2,8 +2,13 @@ import React from "react"; import { Button } from "@carbon/react"; import { ProjectFormModal } from "./ProjectFormModal"; import { JSONModal } from "./JSONModal"; -import { azsort, contains, eachKey, splat, splatContains } from "lazy-z"; -import { Add, MagicWandFilled, Upload } from "@carbon/icons-react"; +import { azsort, contains, eachKey, splatContains } from "lazy-z"; +import { + Add, + DocumentImport, + MagicWandFilled, + Upload, +} from "@carbon/icons-react"; import { ProjectTile } from "./ProjectTile"; import { CraigHeader } from "../SplashPage"; import { templates } from "../../utils"; @@ -49,6 +54,24 @@ class Projects extends React.Component { this.toggleImportJSONModal = this.toggleImportJSONModal.bind(this); this.afterValidation = this.afterValidation.bind(this); this.removeInvalidReferences = this.removeInvalidReferences.bind(this); + this.onFileUpload = this.onFileUpload.bind(this); + this.fileClick = React.createRef(); + } + + onFileUpload(event) { + let parsedJson; + try { + parsedJson = JSON.parse(event.target.result); + } catch (err) { + parsedJson = { + error: err, + }; + } + this.setState({ + showValidationModal: false, + importJSONModalOpen: true, + uploadedJsonData: parsedJson, + }); } shouldComponentUpdate(nextProps, nextState) { @@ -83,9 +106,19 @@ class Projects extends React.Component { } toggleImportJSONModal() { - this.setState({ - importJSONModalOpen: !this.state.importJSONModalOpen, - }); + this.setState( + { + importJSONModalOpen: !this.state.importJSONModalOpen, + }, + () => { + // clear uploaded data only when closing the modal + if (this.state.importJSONModalOpen === false) { + this.setState({ + uploadedJsonData: undefined, + }); + } + } + ); } newProject() { @@ -401,7 +434,7 @@ class Projects extends React.Component {

Projects

-
+
Create a New Project @@ -425,6 +458,8 @@ class Projects extends React.Component { > Import from JSON +
+
+ + (this.fileClick = input)} + onChange={(event) => { + // prevent default + event.preventDefault(event); + // show validation modal + this.setState({ showValidationModal: true }, () => { + // get file and create file reader + let jsonFile = event.target.files[0]; + let reader = new FileReader(); + reader.onload = (event) => { + // callback function on event trigger + this.onFileUpload(event); + }; + // read file + reader.readAsText(jsonFile, "application/json"); + }); + }} + />
{/* hide projects section if there are none */} {projectKeys.length > 0 && ( @@ -545,6 +615,12 @@ class Projects extends React.Component { {this.state.importJSONModalOpen && ( { @@ -552,6 +628,7 @@ class Projects extends React.Component { this.setState( { showValidationModal: true, + uploadedJsonData: undefined, }, () => { this.props.onProjectSave( diff --git a/client/src/components/pages/projects/Wizard.js b/client/src/components/pages/projects/Wizard.js index a02aaab4..a28041be 100644 --- a/client/src/components/pages/projects/Wizard.js +++ b/client/src/components/pages/projects/Wizard.js @@ -53,6 +53,19 @@ class Wizard extends React.Component { this.handleToggle = this.handleToggle.bind(this); this.handlePowerZonesChange = this.handlePowerZonesChange.bind(this); this.onRequestSubmit = this.onRequestSubmit.bind(this); + this.modalShouldBeDisabled = this.modalShouldBeDisabled.bind(this); + } + + modalShouldBeDisabled() { + return ( + // if project name is invalid + invalidProjectName(this.state, this.props) || + // if is power vs and no zones + (this.state.enable_power_vs && isEmpty(this.state.power_vs_zones)) || + // or is fs cloud and no key management service + (this.state.fs_cloud && + isNullOrEmptyString(this.state.key_management_service)) + ); } onRequestSubmit() { @@ -80,7 +93,7 @@ class Wizard extends React.Component { if (name === "region") { this.setState({ region: value, - power_vs_zones: [], + power_vs_zones: ["dal12", "wdc06"], }); } else this.setState({ [name]: value }); } @@ -94,7 +107,7 @@ class Wizard extends React.Component { } else if (name === "power_vs_high_availability" && !this.state[name]) { this.setState({ [name]: !this.state.name, - power_vs_zones: ["dal12", "wdc06"], + power_vs_zones: [], }); } else this.setState({ [name]: !this.state[name] }); } @@ -125,12 +138,7 @@ class Wizard extends React.Component { primaryButtonText="Get Started" onRequestClose={this.props.onRequestClose} onRequestSubmit={this.onRequestSubmit} - primaryButtonDisabled={ - invalidProjectName(this.state, this.props) || - (this.state.enable_power_vs && isEmpty(this.state.power_vs_zones)) || - (this.state.fs_cloud && - isNullOrEmptyString(this.state.key_management_service)) - } + primaryButtonDisabled={this.modalShouldBeDisabled()} size="lg" className="leftTextAlign" > diff --git a/client/src/components/pages/vpc/VpcDeployments.js b/client/src/components/pages/vpc/VpcDeployments.js index 8ae320dc..d8b2e724 100644 --- a/client/src/components/pages/vpc/VpcDeployments.js +++ b/client/src/components/pages/vpc/VpcDeployments.js @@ -532,6 +532,16 @@ class VpcDeploymentsDiagramPage extends React.Component { isSelected={(vpcIndex) => { return vpcIndex === this.state.vpcIndex; }} + onImportedSubnetItemClick={( + vpcIndex, + field, + itemIndex + ) => { + this.setSelection(vpcIndex, field, itemIndex); + }} + parentState={this.state} + tabSelected={this.tabSelected} + onTabClick={this.onSgTabClick} > { let label = props.big diff --git a/client/src/components/utils/downloadCopyButtons/DownloadConfig.js b/client/src/components/utils/downloadCopyButtons/DownloadConfig.js index f60b2ed9..0c324075 100644 --- a/client/src/components/utils/downloadCopyButtons/DownloadConfig.js +++ b/client/src/components/utils/downloadCopyButtons/DownloadConfig.js @@ -7,18 +7,21 @@ const JSZip = require("jszip"); * Download configuration object * @returns (Object|undefined) error */ -export const downloadContent = (json, projectName) => { +export const downloadContent = (json, projectName, imageBlob) => { const zip = new JSZip(); try { let files = configToFilesJson(json); // get files + if (imageBlob) { + zip.file(`${projectName}.png`, imageBlob); + } eachKey(files, (file) => { // add each file's contents to the zip if it is not null or empty string if ( isNullOrEmptyString(files[file]) === false && (contains(file, ".") || contains(file, "LICENSE")) - ) + ) { zip.file(file, files[file]); - else if (isNullOrEmptyString(files[file]) === false) { + } else if (isNullOrEmptyString(files[file]) === false) { zip.folder(file); eachKey(files[file], (subFile) => { zip.file(file + "/" + subFile, files[file][subFile]); diff --git a/client/src/lib/components/toggle-form-components.js b/client/src/lib/components/toggle-form-components.js index fc35367a..deef30c8 100644 --- a/client/src/lib/components/toggle-form-components.js +++ b/client/src/lib/components/toggle-form-components.js @@ -47,14 +47,17 @@ function dynamicPrimaryButtonProps(props) { /** * get props for delete button * @param {*} props + * @param {*} isV2Page * @returns {object} props object */ -function dynamicSecondaryButtonProps(props) { +function dynamicSecondaryButtonProps(props, isV2Page) { return { popoverProps: { hoverText: props.disabled && props.disableDeleteMessage ? props.disableDeleteMessage + : isV2Page + ? "Delete Resource" : "Delete " + props.name, className: props.disabled ? "inlineBlock cursorNotAllowed" : "", }, diff --git a/client/src/lib/constants.js b/client/src/lib/constants.js index 6f1082af..d1038335 100644 --- a/client/src/lib/constants.js +++ b/client/src/lib/constants.js @@ -730,6 +730,8 @@ module.exports = { "mad04", "sao04", "dal12", + "sao01", + "tok04", ], cosPlans: [ "standard", @@ -839,10 +841,40 @@ module.exports = { wdc07: ["Tier3-Flash-2", "Tier3-Flash-1", "Tier1-Flash-2", "Tier1-Flash-1"], }, replicationEnabledStoragePoolMap: { - "us-east": ["Tier1-Flash-8"], - wdc06: ["Tier1-Flash-1", "Tier3-Flash-2", "Tier3-Flash-1"], - "us-south": ["Tier1-Flash-6"], - dal12: ["Tier1-Flash-3", "Tier3-Flash-4", "Tier3-Flash-3"], + "us-east": ["Tier1-Flash-8", "General-Flash-185", "General-Flash-182"], + wdc06: [ + "Tier1-Flash-1", + "Tier3-Flash-2", + "Tier3-Flash-1", + "General-Flash-83", + "General-Flash-77", + "General-Flash-74", + ], + wdc07: [ + "General-Flash-83", + "General-Flash-80", + "General-Flash-77", + "General-Flash-74", + ], + "us-south": ["Tier1-Flash-6", "General-Flash-26", "General-Flash-23"], + dal12: [ + "Tier1-Flash-3", + "Tier3-Flash-4", + "Tier3-Flash-3", + "General-Flash-93", + "General-Flash-87", + "General-Flash-84", + ], + dal10: [ + "General-Flash-53", + "General-Flash-59", + "General-Flash-56", + "General-Flash-50", + ], + mad02: ["General-Flash-56", "General-Flash-53", "General-Flash-50"], + mad04: ["General-Flash-59", "General-Flash-56", "General-Flash-50"], + "eu-de-1": ["General-Flash-90", "General-Flash-96", "General-Flash-87"], + "eu-de-2": ["General-Flash-96", "General-Flash-93", "General-Flash-90"], }, template_dropdown_map: { Mixed: { diff --git a/client/src/lib/docs/docs.json b/client/src/lib/docs/docs.json index 4b7442ec..503ffc1d 100644 --- a/client/src/lib/docs/docs.json +++ b/client/src/lib/docs/docs.json @@ -2314,5 +2314,21 @@ } ], "relatedLinks": [] + }, + "classic_vsi": { + "content": [ + { + "text": "NYI" + } + ], + "relatedLinks": [] + }, + "classic_bare_metal": { + "content": [ + { + "text": "NYI" + } + ], + "relatedLinks": [] } } diff --git a/client/src/lib/docs/release-notes.json b/client/src/lib/docs/release-notes.json index 10e70fa2..c9c2d3cc 100644 --- a/client/src/lib/docs/release-notes.json +++ b/client/src/lib/docs/release-notes.json @@ -1,4 +1,29 @@ [ + { + "version": "1.13.0", + "features": [ + "Users will now get feedback when a Power VS Workspace has no VTL images when trying to create or update a VTL instance. Images can be added from the Power VS Workspace form", + "Icons for deployments within imported subnets are now rendered within that subnet on the `/v2/vpcDeployments` page", + "Users can now create Classic VSI from the form page `/form/classicVsi`", + "Power VS Workspace names, ids, and CRNs are now included as outputs in the `outputs.tf` file of any CRAIG Terraform template", + "Power VS High Availability is now supported for `mad02`, `mad04`, `us-east`, `wdc06`, `us-south`, `eu-de-1`, and `eu-de-2`", + "When updating a Power VS Instance name, references to that instance are now updated to match the new name", + "Users can now upload JSON directly to CRAIG from the local file explorer from the Projects page by clicking the new `Upload JSON` button", + "Users can now create Classic Bare Metal Servers from the form page `/form/classicBareMetal`", + "Power VS Instance Primary IP addresses are now included as outputs in the `outputs.tf` file of any CRAIG Terraform template", + "When downloading a `.zip` file of an environment from the CRAIG GUI, an image of the current environment is now included in the archive", + "CRAIG now supports adding a Pin Policy to Power VS instances" + ], + "fixes": [ + "Fixed an issue with button hoverText alignment causing overflow in forms", + "Fixed an issue causing an incorrect name for DNS Services in the `/v2/services` page", + "Fixed an issue preventing users from inputing decimal values for CPU when creating a FalconStor VTL instance with a shared processor type" + ], + "upgrade_notes": [ + "A new, improved version of the CRAIG tutorial is available in the README. This tutorial shows users how to install CRAIG, use the new V2 GUI, and integrate deployments with schematics", + "Deprecated field for VSI primary IPs has been change to a version that no longer throws Terraform warnings" + ] + }, { "version": "1.12.2", "features": [ diff --git a/client/src/lib/docs/templates/power-poc-quick-start.json b/client/src/lib/docs/templates/power-poc-quick-start.json index 02d39fa2..d20d6ea1 100644 --- a/client/src/lib/docs/templates/power-poc-quick-start.json +++ b/client/src/lib/docs/templates/power-poc-quick-start.json @@ -10,7 +10,7 @@ "enable_power_vs": true, "enable_classic": false, "power_vs_zones": ["dal10"], - "craig_version": "1.12.2", + "craig_version": "1.13.0", "power_vs_high_availability": false, "template": "Power VS POC", "fs_cloud": false diff --git a/client/src/lib/forms/disable-save.js b/client/src/lib/forms/disable-save.js index 96a4ea2d..ef29a478 100644 --- a/client/src/lib/forms/disable-save.js +++ b/client/src/lib/forms/disable-save.js @@ -96,6 +96,8 @@ function disableSave(field, stateData, componentProps, craig) { "fortigate_vnf", "classic_security_groups", "classic_sg_rules", + "classic_vsi", + "classic_bare_metal", ]; let isPowerSshKey = field === "ssh_keys" && componentProps.arrayParentName; if (contains(stateDisableSaveComponents, field) || isPowerSshKey) { diff --git a/client/src/lib/forms/dynamic-form-fields/index.js b/client/src/lib/forms/dynamic-form-fields/index.js index ae77a9c0..1efa6def 100644 --- a/client/src/lib/forms/dynamic-form-fields/index.js +++ b/client/src/lib/forms/dynamic-form-fields/index.js @@ -15,8 +15,10 @@ const { dynamicTextAreaProps } = require("./text-area"); const { dynamicHeadingProps } = require("./heading"); const { dynamicCraigFormGroupsProps } = require("./craig-form-group"); const { dynamicToolTipWrapperProps } = require("./dynamic-tooltip-wrapper"); +const { dynamicPasswordInputProps } = require("./password-input"); module.exports = { + dynamicPasswordInputProps, dynamicToolTipWrapperProps, dynamicCraigFormGroupsProps, dynamicHeadingProps, diff --git a/client/src/lib/forms/dynamic-form-fields/toggle.js b/client/src/lib/forms/dynamic-form-fields/toggle.js index 1c3eb158..1a27b628 100644 --- a/client/src/lib/forms/dynamic-form-fields/toggle.js +++ b/client/src/lib/forms/dynamic-form-fields/toggle.js @@ -1,4 +1,4 @@ -const { kebabCase, paramTest } = require("lazy-z"); +const { kebabCase, paramTest, isFunction } = require("lazy-z"); const { disabledReturnsBooleanCheck, addClassName } = require("./utils"); /** @@ -70,6 +70,9 @@ function dynamicToggleProps(props) { ? props.field.onRender(props.parentState, props.parentProps) : props.parentState[props.name], disabled: isDisabled, + key: isFunction(props.field.forceUpdateKey) + ? props.field.forceUpdateKey(props.parentState, props.parentProps) + : undefined, }; } diff --git a/client/src/lib/forms/index.js b/client/src/lib/forms/index.js index f57e747c..371e22e4 100644 --- a/client/src/lib/forms/index.js +++ b/client/src/lib/forms/index.js @@ -37,8 +37,42 @@ const { getDisplayTierSubnetList, shouldDisplayService, } = require("./diagrams"); +const { + dynamicToolTipWrapperProps, + dynamicCraigFormGroupsProps, + dynamicHeadingProps, + dynamicTextAreaProps, + dynamicSelectProps, + dynamicMultiSelectProps, + dynamicToggleProps, + fieldFunctionReturnsBooleanCheck, + disabledReturnsBooleanCheck, + invalidReturnsBooleanCheck, + fieldFunctionReturnsStringCheck, + groupsEvaluatesToArrayCheck, + dynamicFieldId, + addClassName, + dynamicTextInputProps, + dynamicPasswordInputProps, +} = require("./dynamic-form-fields"); module.exports = { + dynamicPasswordInputProps, + dynamicToolTipWrapperProps, + dynamicCraigFormGroupsProps, + dynamicHeadingProps, + dynamicTextAreaProps, + dynamicSelectProps, + dynamicMultiSelectProps, + dynamicToggleProps, + fieldFunctionReturnsBooleanCheck, + disabledReturnsBooleanCheck, + invalidReturnsBooleanCheck, + fieldFunctionReturnsStringCheck, + groupsEvaluatesToArrayCheck, + dynamicFieldId, + addClassName, + dynamicTextInputProps, shouldDisplayService, getDisplayTierSubnetList, getDisplaySubnetTiers, diff --git a/client/src/lib/index.js b/client/src/lib/index.js index 922443fa..a7c7be16 100644 --- a/client/src/lib/index.js +++ b/client/src/lib/index.js @@ -26,6 +26,22 @@ const { getDisplaySubnetTiers, getDisplayTierSubnetList, shouldDisplayService, + dynamicToolTipWrapperProps, + dynamicCraigFormGroupsProps, + dynamicHeadingProps, + dynamicTextAreaProps, + dynamicSelectProps, + dynamicMultiSelectProps, + dynamicToggleProps, + fieldFunctionReturnsBooleanCheck, + disabledReturnsBooleanCheck, + invalidReturnsBooleanCheck, + fieldFunctionReturnsStringCheck, + groupsEvaluatesToArrayCheck, + dynamicFieldId, + addClassName, + dynamicTextInputProps, + dynamicPasswordInputProps, } = require("./forms"); const { slzToCraig } = require("./slz-to-craig"); const validate = require("./validate"); @@ -110,6 +126,8 @@ const { formatClassicSgRule, formatClassicSg, classicSecurityGroupTf, + classicVsiTf, + classicBareMetalTf, } = require("./json-to-iac"); const releaseNotes = require("./docs/release-notes.json"); const docs = require("./docs/docs.json"); @@ -118,6 +136,24 @@ const { invalidForms } = require("./invalid-forms"); const { allDocText, filterDocs } = require("./docs"); module.exports = { + dynamicPasswordInputProps, + dynamicToolTipWrapperProps, + dynamicCraigFormGroupsProps, + dynamicHeadingProps, + dynamicTextAreaProps, + dynamicSelectProps, + dynamicMultiSelectProps, + dynamicToggleProps, + fieldFunctionReturnsBooleanCheck, + disabledReturnsBooleanCheck, + invalidReturnsBooleanCheck, + fieldFunctionReturnsStringCheck, + groupsEvaluatesToArrayCheck, + dynamicFieldId, + addClassName, + dynamicTextInputProps, + classicVsiTf, + classicBareMetalTf, classicSecurityGroupTf, formatClassicSgRule, formatClassicSg, diff --git a/client/src/lib/json-to-iac/classic-bare-metal.js b/client/src/lib/json-to-iac/classic-bare-metal.js new file mode 100644 index 00000000..11861f06 --- /dev/null +++ b/client/src/lib/json-to-iac/classic-bare-metal.js @@ -0,0 +1,60 @@ +const { snakeCase } = require("lazy-z"); +const { jsonToTfPrint, kebabName, tfBlock } = require("./utils"); + +/** + * format classic bare metal servers + * @param {*} vsi + * @param {*} config + * @returns {string} terraform formatted string + */ +function formatClassicBareMetal(server, config) { + let bareMetalData = { + package_key_name: server.package_key_name, + process_key_name: server.process_key_name, + os_key_name: server.os_key_name, + memory: server.memory || 64, + hostname: kebabName([server.name]), + domain: server.domain, + datacenter: server.datacenter, + network_speed: server.network_speed || 100, + public_bandwidth: server.private_network_only + ? undefined + : server.public_bandwidth || 500, + disk_key_names: server.disk_key_names, + hourly_billing: false, + private_network_only: server.private_network_only, + private_vlan_id: `\${ibm_network_vlan.classic_vlan_${snakeCase( + server.private_vlan + )}.id}`, + public_vlan_id: server.private_network_only + ? undefined + : `\${ibm_network_vlan.classic_vlan_${snakeCase(server.public_vlan)}.id}`, + }; + + return jsonToTfPrint( + "resource", + "ibm_compute_bare_metal", + server.name, + bareMetalData + ); +} + +/** + * create classic bare metal tf + * @param {*} config + * @returns {string} terraform formatted string + */ +function classicBareMetalTf(config) { + let tf = ""; + if (config.classic_bare_metal) { + config.classic_bare_metal.forEach((server) => { + tf += formatClassicBareMetal(server, config); + }); + } + return tf.length === 0 ? null : tfBlock(`Classic Bare Metal Servers`, tf); +} + +module.exports = { + formatClassicBareMetal, + classicBareMetalTf, +}; diff --git a/client/src/lib/json-to-iac/classic-vsi.js b/client/src/lib/json-to-iac/classic-vsi.js new file mode 100644 index 00000000..6a2076e9 --- /dev/null +++ b/client/src/lib/json-to-iac/classic-vsi.js @@ -0,0 +1,75 @@ +const { snakeCase } = require("lazy-z"); +const { jsonToTfPrint, kebabName, tfBlock } = require("./utils"); + +/** + * format classic vsi + * @param {*} vsi + * @param {*} config + * @returns {string} terraform formatted string + */ +function formatClassicVsi(vsi, config) { + let vsiData = { + hostname: kebabName([vsi.name]), + datacenter: vsi.datacenter, + domain: vsi.domain, + cores: Number(vsi.cores), + memory: Number(vsi.memory), + image_id: vsi.image_id, + local_disk: vsi.local_disk, + network_speed: Number(vsi.network_speed), + private_network_only: vsi.private_network_only, + private_vlan_id: `\${ibm_network_vlan.classic_vlan_${snakeCase( + vsi.private_vlan + )}.id}`, + public_vlan_id: vsi.private_network_only + ? undefined + : `\${ibm_network_vlan.classic_vlan_${snakeCase(vsi.public_vlan)}.id}`, + public_security_group_ids: vsi.private_network_only ? undefined : [], + private_security_group_ids: [], + ssh_key_ids: [], + }; + vsi.private_security_groups.forEach((sg) => { + vsiData.private_security_group_ids.push( + `\${ibm_security_group.classic_securtiy_group_${snakeCase(sg)}.id}` + ); + }); + if (!vsi.private_network_only) { + vsi.public_security_groups.forEach((sg) => { + vsiData.public_security_group_ids.push( + `\${ibm_security_group.classic_securtiy_group_${snakeCase(sg)}.id}` + ); + }); + } + vsi.ssh_keys.forEach((key) => { + vsiData.ssh_key_ids.push( + `\${ibm_compute_ssh_key.classic_ssh_key_${snakeCase(key)}.id}` + ); + }); + vsiData.tags = config._options.tags; + return jsonToTfPrint( + "resource", + "ibm_compute_vm_instance", + `classic_vsi_${snakeCase(vsi.name)}`, + vsiData + ); +} + +/** + * create classic vsi tf + * @param {*} config + * @returns {string} terraform formatted string + */ +function classicVsiTf(config) { + let tf = ""; + if (config.classic_vsi) { + config.classic_vsi.forEach((vsi) => { + tf += formatClassicVsi(vsi, config); + }); + } + return tf.length === 0 ? null : tfBlock(`Classic VSI`, tf); +} + +module.exports = { + formatClassicVsi, + classicVsiTf, +}; diff --git a/client/src/lib/json-to-iac/config-to-files-json.js b/client/src/lib/json-to-iac/config-to-files-json.js index fa0d6f7e..27f48676 100644 --- a/client/src/lib/json-to-iac/config-to-files-json.js +++ b/client/src/lib/json-to-iac/config-to-files-json.js @@ -36,6 +36,8 @@ const { scc2Tf } = require("./scc-v2"); const { fortigateTf } = require("./fortigate"); const { outputsTf } = require("./outputs"); const { classicSecurityGroupTf } = require("./classic-security-group"); +const { classicVsiTf } = require("./classic-vsi"); +const { classicBareMetalTf } = require("./classic-bare-metal"); const apacheLicense = ` Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -319,6 +321,8 @@ function configToFilesJson(config, apiMode, templateTarMode) { "fortigate_vnf.tf": fortigateTf(config), "outputs.tf": outputsTf(config), "classic_security_groups.tf": classicSecurityGroupTf(config), + "classic_vsi.tf": classicVsiTf(config), + "classic_bare_metal.tf": classicBareMetalTf(config), }; vpcModuleTf(files, config); return files; diff --git a/client/src/lib/json-to-iac/index.js b/client/src/lib/json-to-iac/index.js index e8c792c0..ee916b19 100644 --- a/client/src/lib/json-to-iac/index.js +++ b/client/src/lib/json-to-iac/index.js @@ -192,8 +192,11 @@ const { formatClassicSgRule, classicSecurityGroupTf, } = require("./classic-security-group"); - +const { classicVsiTf } = require("./classic-vsi"); +const { classicBareMetalTf } = require("./classic-bare-metal"); module.exports = { + classicVsiTf, + classicBareMetalTf, classicSecurityGroupTf, formatClassicSgRule, formatClassicSg, diff --git a/client/src/lib/json-to-iac/outputs.js b/client/src/lib/json-to-iac/outputs.js index 4c2c2cda..0d253b7a 100644 --- a/client/src/lib/json-to-iac/outputs.js +++ b/client/src/lib/json-to-iac/outputs.js @@ -59,6 +59,8 @@ function outputsTf(config) { "\n" ) + (config.vpcs[index + 1] ? "\n" : ""); }); + + // vsi outputs config.vsi.forEach((deployment) => { let deploymentOutputs = {}; deployment.subnets.sort(azsort).forEach((subnet, subnetIndex) => { @@ -74,7 +76,7 @@ function outputsTf(config) { `${deployment.vpc} vpc ${deployment.name} vsi ${subnetIndex + 1} ${ i + 1 }` - )}.primary_network_interface[0].primary_ipv4_address}`, + )}.primary_network_interface[0].primary_ip[0].address}`, }; if (deployment.enable_floating_ip) { deploymentOutputs[ @@ -106,9 +108,10 @@ function outputsTf(config) { "\n" ); }); + + // power workspaces config.power.forEach((workspace, index) => { let outputs = {}; - // power workspace outputs ["name", "guid", "crn"].forEach((field) => { outputs["power_vs_workspace_" + snakeCase(workspace.name) + "_" + field] = @@ -135,6 +138,26 @@ function outputsTf(config) { ) + (config.power[index + 1] ? "\n" : ""); }); + + let powerInstanceOutputs = {}; + // power instances + if (config.power_instances) + config.power_instances.forEach((instance, index) => { + let vsiRef = `${snakeCase( + instance.workspace + )}_workspace_instance_${snakeCase(instance.name)}`; + powerInstanceOutputs[vsiRef + "_primary_ip"] = { + value: `\${ibm_pi_instance.${vsiRef}.pi_network[0].ip_address}`, + }; + }); + if (Object.keys(powerInstanceOutputs).length > 0) + tf += + "\n" + + tfBlock( + "Power VS Instance Outputs", + "\n" + jsonToTf(JSON.stringify({ output: powerInstanceOutputs })) + "\n" + ); + return tf; } diff --git a/client/src/lib/nav-catagories.js b/client/src/lib/nav-catagories.js index 20d14086..1c344849 100644 --- a/client/src/lib/nav-catagories.js +++ b/client/src/lib/nav-catagories.js @@ -30,6 +30,8 @@ const { powerVsVolumeTf, classicInfraTf, classicSecurityGroupTf, + classicVsiTf, + classicBareMetalTf, } = require("./json-to-iac"); const { cisTf } = require("./json-to-iac/cis"); const { classicGatewayTf } = require("./json-to-iac/classic-gateway"); @@ -300,6 +302,23 @@ const navCatagories = [ }, jsonField: "classic_security_groups", }, + { + title: "Classic Virtual Servers", + path: "/form/classicVsi", + react_icon: "InstanceClassic", + toTf: (config) => { + return classicVsiTf(config) || ""; + }, + }, + { + title: "Classic Bare Metal", + path: "/form/classicBareMetal", + react_icon: "IbmCloudBareMetalServer", + toTf: (config) => { + return classicBareMetalTf(config) || ""; + }, + jsonField: "classic_bare_metal", + }, ], }, { diff --git a/client/src/lib/state/appid.js b/client/src/lib/state/appid.js index 1f6819b0..fb52cdeb 100644 --- a/client/src/lib/state/appid.js +++ b/client/src/lib/state/appid.js @@ -1,4 +1,4 @@ -const { transpose, splatContains, isNullOrEmptyString } = require("lazy-z"); +const { transpose, splatContains } = require("lazy-z"); const { setUnfoundResourceGroup, updateSubChild, diff --git a/client/src/lib/state/classic-bare-metal.js b/client/src/lib/state/classic-bare-metal.js new file mode 100644 index 00000000..2052ec00 --- /dev/null +++ b/client/src/lib/state/classic-bare-metal.js @@ -0,0 +1,166 @@ +const { isNullOrEmptyString } = require("lazy-z"); +const { + nameField, + unconditionalInvalidText, + fieldIsNullOrEmptyString, + classicDatacenterField, + domainField, + classicPrivateNetworkOnly, + classicPrivateVlan, + classicPublicVlan, +} = require("./reusable-fields"); +const { shouldDisableComponentSave, onArrayInputChange } = require("./utils"); + +/** + * init store + * @param {*} store + */ +function initClassicBareMetalStore(store) { + store.newField("classic_bare_metal", { + init: function (config) { + config.store.json.classic_bare_metal = []; + }, + onStoreUpdate: function (config) { + if (!config.store.json.classic_bare_metal) { + config.store.json.classic_bare_metal = []; + } + }, + create: function (config, stateData, componentProps) { + config.push(["json", "classic_bare_metal"], stateData); + }, + save: function (config, stateData, componentProps) { + config.updateChild( + ["json", "classic_bare_metal"], + componentProps.data.name, + stateData + ); + }, + delete: function (config, stateData, componentProps) { + config.carve(["json", "classic_bare_metal"], componentProps.data.name); + }, + shouldDisableSave: shouldDisableComponentSave( + [ + "name", + "domain", + "datacenter", + "os_key_name", + "package_key_name", + "process_key_name", + "public_vlan", + "private_vlan", + "disk_key_names", + "public_bandwidth", + "memory", + "network_speed", + ], + "classic_bare_metal" + ), + schema: { + name: nameField("classic_bare_metal", { + size: "small", + }), + domain: domainField(), + datacenter: classicDatacenterField(), + os_key_name: { + size: "small", + labelText: "OS Key Name", + invalid: (stateData) => { + return isNullOrEmptyString(stateData.os_key_name); + }, + invalidText: unconditionalInvalidText("Enter an OS Key Name"), + default: "", + tooltip: { + content: + "The operating system key name that you want to use to provision the computing instance. To get OS key names, find the package key name in the IBM Cloud Classic API.", + align: "right", + alignModal: "right", + }, + }, + package_key_name: { + default: "", + size: "small", + invalid: fieldIsNullOrEmptyString("package_key_name"), + invalidText: unconditionalInvalidText("Enter a Package Key Name"), + tooltip: { + content: + "The key name for the monthly Bare Metal server's package. You can find available package key names in the IBM Cloud Classic API'", + align: "right", + }, + }, + process_key_name: { + default: "", + size: "small", + invalid: fieldIsNullOrEmptyString("process_key_name"), + invalidText: unconditionalInvalidText("Enter a Process Key Name"), + tooltip: { + content: + "The key name for the monthly Bare Metal server's process. To get a process key name, find the package key name in the IBM Cloud Classic API", + align: "right", + }, + }, + memory: { + size: "small", + default: "", + labelText: "Memory (GB)", + placeholder: "64", + invalid: fieldIsNullOrEmptyString("memory"), + invalidText: unconditionalInvalidText("Invalid memory value"), + }, + network_speed: { + size: "small", + default: "", + labelText: "Network Speed (Mbs)", + placeholder: "100", + invalid: fieldIsNullOrEmptyString("network_speed"), + invalidText: unconditionalInvalidText("Invalid network speed value"), + }, + disk_key_names: { + placeholder: "disk-key-1, disk-key-2", + default: [], + type: "textArea", + labelText: "Disk Key Names", + invalid: function (stateData, componentProps) { + return ( + !stateData.disk_key_names || stateData.disk_key_names.length === 0 + ); + }, + invalidText: unconditionalInvalidText( + "Enter a comma separated list of disk key names" + ), + helperText: unconditionalInvalidText( + "Enter a comma separated list of disk key names" + ), + onInputChange: onArrayInputChange("disk_key_names"), + tooltip: { + content: + "The internal key names for the monthly Bare Metal server's disk. To get disk key names, find the package key name in the IBM Cloud Classic API", + align: "right", + alignModal: "right", + }, + }, + private_network_only: classicPrivateNetworkOnly(), + private_vlan: classicPrivateVlan(), + public_vlan: classicPublicVlan(), + public_bandwidth: { + default: "", + placeholder: "500", + size: "small", + labelText: "Public Bandwidth (GB/month)", + hideWhen: function (stateData) { + return stateData.private_network_only; + }, + invalid: (stateData) => { + return ( + !stateData.private_network_only && + isNullOrEmptyString(stateData.public_bandwidth) + ); + }, + invalidText: unconditionalInvalidText("Invalid public bandwidth value"), + }, + }, + }); +} + +module.exports = { + initClassicBareMetalStore, +}; diff --git a/client/src/lib/state/classic-gateways.js b/client/src/lib/state/classic-gateways.js index 47d0cb0a..51d44dc9 100644 --- a/client/src/lib/state/classic-gateways.js +++ b/client/src/lib/state/classic-gateways.js @@ -5,15 +5,20 @@ const { isInRange, splat, } = require("lazy-z"); -const { RegexButWithWords } = require("regex-but-with-words"); const { fieldIsNullOrEmptyString, shouldDisableComponentSave, unconditionalInvalidText, selectInvalidText, } = require("./utils"); -const { datacenters } = require("../constants"); -const { nameField } = require("./reusable-fields"); +const { + nameField, + domainField, + classicDatacenterField, + classicPublicVlan, + classicPrivateVlan, + classicPrivateNetworkOnly, +} = require("./reusable-fields"); /** * init store @@ -94,23 +99,6 @@ function classicGatewayDelete(config, stateData, componentProps) { config.carve(["json", "classic_gateways"], componentProps.data.name); } -/** - * classic vlan filter function - * @param {string} type - * @returns {Function} groups function - */ -function classicVlanFilter(type) { - return function (stateData, componentProps) { - return splat( - componentProps.craig.store.json.classic_vlans.filter((vlan) => { - if (vlan.datacenter === stateData.datacenter && vlan.type === type) - return vlan; - }), - "name" - ); - }; -} - /** * init classic gateway store * @param {*} store @@ -147,39 +135,8 @@ function initClassicGateways(store) { }, size: "small", }), - domain: { - default: "", - invalid: function (stateData) { - return ( - // prevent returning error - (stateData.domain || "").match( - new RegexButWithWords() - .stringBegin() - .set("a-z") - .oneOrMore() - .literal(".") - .set("a-z") - .oneOrMore() - .stringEnd() - .done("g") - ) === null - ); - }, - invalidText: unconditionalInvalidText("Enter a valid domain"), - size: "small", - }, - datacenter: { - default: "", - type: "select", - invalid: fieldIsNullOrEmptyString("datacenter"), - invalidText: selectInvalidText("datacenter"), - onStateChange: function (stateData) { - stateData.private_vlan = ""; - stateData.public_vlan = ""; - }, - groups: datacenters, - size: "small", - }, + domain: domainField(), + datacenter: classicDatacenterField(), network_speed: { default: "", type: "select", @@ -232,15 +189,7 @@ function initClassicGateways(store) { groups: ["INTEL_XEON_4210_2_20"], size: "small", }, - private_vlan: { - labelText: "Private VLAN", - default: "", - type: "select", - invalid: fieldIsNullOrEmptyString("private_vlan"), - invalidText: selectInvalidText("private VLAN"), - groups: classicVlanFilter("PRIVATE"), - size: "small", - }, + private_vlan: classicPrivateVlan(), ssh_key: { labelText: "SSH Key", default: "", @@ -255,22 +204,7 @@ function initClassicGateways(store) { }, size: "small", }, - public_vlan: { - labelText: "Public VLAN", - default: "", - type: "select", - invalid: function (stateData) { - return stateData.private_network_only - ? false - : fieldIsNullOrEmptyString("public_vlan")(stateData); - }, - invalidText: selectInvalidText("public VLAN"), - groups: classicVlanFilter("PUBLIC"), - size: "small", - hideWhen: function (stateData) { - return stateData.private_network_only; - }, - }, + public_vlan: classicPublicVlan(), disk_key_names: { default: [], type: "multiselect", @@ -281,18 +215,7 @@ function initClassicGateways(store) { groups: ["HARD_DRIVE_2_00_TB_SATA_2"], size: "small", }, - private_network_only: { - type: "toggle", - labelText: "Private Network Only", - default: false, - onStateChange: function (stateData) { - if (stateData.private_network_only !== true) { - stateData.private_network_only = true; - stateData.public_vlan = ""; - } else stateData.private_network_only = false; - }, - size: "small", - }, + private_network_only: classicPrivateNetworkOnly(), tcp_monitoring: { size: "small", default: false, diff --git a/client/src/lib/state/classic-vsi.js b/client/src/lib/state/classic-vsi.js new file mode 100644 index 00000000..ee475758 --- /dev/null +++ b/client/src/lib/state/classic-vsi.js @@ -0,0 +1,203 @@ +const { splatContains, isEmpty, splat } = require("lazy-z"); +const { + shouldDisableComponentSave, + fieldIsNotWholeNumber, +} = require("./utils"); +const { + nameField, + domainField, + classicDatacenterField, + classicPrivateVlan, + classicPublicVlan, + fieldIsNullOrEmptyString, + unconditionalInvalidText, + classicPrivateNetworkOnly, +} = require("./reusable-fields"); + +/** + * init classic vsi + * @param {*} store + */ +function initClassicVsi(store) { + store.newField("classic_vsi", { + init: function (config) { + config.store.json.classic_vsi = []; + }, + create: function (config, stateData, componentProps) { + config.push(["json", "classic_vsi"], stateData); + }, + save: function (config, stateData, componentProps) { + config.updateChild( + ["json", "classic_vsi"], + componentProps.data.name, + stateData + ); + }, + delete: function (config, stateData, componentProps) { + config.carve(["json", "classic_vsi"], componentProps.data.name); + }, + onStoreUpdate: function (config) { + if (config.store.json.classic_vsi) { + config.store.json.classic_vsi.forEach((vsi) => { + ["public_vlan", "private_vlan"].forEach((field) => { + if ( + !splatContains( + config.store.json.classic_vlans, + "name", + vsi[field] + ) + ) + vsi[field] = null; + }); + let nextSgs = { + public_security_groups: [], + private_security_groups: [], + }; + ["public_security_groups", "private_security_groups"].forEach( + (field) => { + vsi[field].forEach((item) => { + if ( + splatContains( + config.store.json.classic_security_groups, + "name", + item + ) + ) { + nextSgs[field].push(item); + } + }); + vsi[field] = nextSgs[field]; + } + ); + let nextSshKeys = []; + vsi.ssh_keys.forEach((key) => { + if ( + splatContains(config.store.json.classic_ssh_keys, "name", key) + ) { + nextSshKeys.push(key); + } + }); + vsi.ssh_keys = nextSshKeys; + }); + } else config.store.json.classic_vsi = []; + }, + shouldDisableSave: shouldDisableComponentSave( + [ + "name", + "datacenter", + "domain", + "cores", + "memory", + "image_id", + "network_speed", + "private_vlan", + "public_vlan", + "private_security_groups", + "public_security_groups", + "ssh_keys", + ], + "classic_vsi" + ), + schema: { + name: nameField("classic_vsi", { + size: "small", + }), + domain: domainField(), + datacenter: classicDatacenterField(), + private_vlan: classicPrivateVlan(), + public_vlan: classicPublicVlan(), + cores: { + default: "", + invalid: fieldIsNullOrEmptyString("cores"), + size: "small", + }, + memory: { + default: "", + invalid: fieldIsNullOrEmptyString("memory"), + size: "small", + }, + image_id: { + default: "", + labelText: "Image ID", + invalid: fieldIsNullOrEmptyString("image_id"), + size: "small", + }, + network_speed: { + default: "100", + invalid: fieldIsNotWholeNumber("network_speed", 0, 10000), + invalidText: unconditionalInvalidText("Must be a whole number"), + size: "small", + }, + local_disk: { + type: "toggle", + size: "small", + labelText: "Local Disk", + }, + private_network_only: classicPrivateNetworkOnly(), + ssh_keys: { + labelText: "SSH Keys", + type: "multiselect", + size: "small", + default: [], + invalid: function (stateData) { + return !stateData.ssh_keys || isEmpty(stateData.ssh_keys); + }, + invalidText: unconditionalInvalidText("Select at least one SSH key"), + groups: function (stateData, componentProps) { + return splat( + componentProps.craig.store.json.classic_ssh_keys, + "name" + ); + }, + }, + private_security_groups: { + type: "multiselect", + size: "small", + default: [], + invalid: function (stateData) { + return ( + !stateData.private_security_groups || + isEmpty(stateData.private_security_groups) + ); + }, + invalidText: unconditionalInvalidText( + "Select at least one security group" + ), + groups: function (stateData, componentProps) { + return splat( + componentProps.craig.store.json.classic_security_groups, + "name" + ); + }, + }, + public_security_groups: { + type: "multiselect", + size: "small", + default: [], + invalid: function (stateData) { + return ( + !stateData.private_network_only && + (!stateData.public_security_groups || + isEmpty(stateData.public_security_groups)) + ); + }, + invalidText: unconditionalInvalidText( + "Select at least one security group" + ), + groups: function (stateData, componentProps) { + return splat( + componentProps.craig.store.json.classic_security_groups, + "name" + ); + }, + hideWhen: function (stateData) { + return stateData.private_network_only; + }, + }, + }, + }); +} + +module.exports = { + initClassicVsi, +}; diff --git a/client/src/lib/state/options.js b/client/src/lib/state/options.js index 8cac6d18..1f5b96b7 100644 --- a/client/src/lib/state/options.js +++ b/client/src/lib/state/options.js @@ -32,6 +32,14 @@ const powerVsZones = [ "ca-tor", ]; +const powerHaMap = { + mad02: "eu-de-1", + mad04: "eu-de-2", + "us-east": "us-south", + wdc06: "dal12", + wdc07: "dal10", +}; + /** * initialize options * @param {lazyZstate} config @@ -134,6 +142,19 @@ function hideWhenNotPowerVs(stateData) { return stateData.enable_power_vs !== true; } +/** + * hide a field when power vs ha not enabled + * @param {*} stateData + * @param {*} componentProps + * @returns {boolean} true when not enabled + */ +function hideWhenNotPowerHa(stateData) { + return ( + hideWhenNotPowerVs(stateData) || + stateData.power_vs_high_availability !== true + ); +} + /** * init options store * @param {*} store @@ -143,7 +164,7 @@ function initOptions(store) { init: optionsInit, save: optionsSave, shouldDisableSave: shouldDisableComponentSave( - ["prefix", "tags", "power_vs_zones", "region"], + ["prefix", "tags", "power_vs_zones", "region", "power_vs_ha_zone_1"], "options" ), schema: { @@ -254,6 +275,7 @@ function initOptions(store) { stateData.power_vs_zones = []; stateData.enable_power_vs = false; } else { + stateData.power_vs_zones = []; stateData.enable_power_vs = true; } }, @@ -265,24 +287,61 @@ function initOptions(store) { labelText: "High Availability", tooltip: { content: - "Enable High Availability and Disaster Recovery for Power VS by using enabled zones Dallas 12 and Washington DC 6", + "Enable High Availability and Disaster Recovery for Power VS by using enabled zones", }, hideWhen: hideWhenNotPowerVs, onStateChange: function (stateData) { if (!stateData.power_vs_high_availability) { stateData.power_vs_high_availability = true; - stateData.power_vs_zones = ["dal12", "wdc06"]; + stateData.power_vs_zones = []; } else { stateData.power_vs_high_availability = false; stateData.power_vs_zones = []; } }, }, + power_vs_ha_zone_1: { + size: "small", + type: "select", + labelText: "Power VS Site 1", + hideWhen: hideWhenNotPowerHa, + groups: ["mad02", "mad04", "us-east", "wdc06", "wdc07"], + onRender: function (stateData) { + return stateData.power_vs_zones[0] || ""; + }, + onStateChange: function (stateData) { + stateData.power_vs_zones = [stateData.power_vs_ha_zone_1]; + stateData.power_vs_zones.push( + powerHaMap[stateData.power_vs_ha_zone_1] + ); + }, + invalid: function (stateData) { + return !stateData.enable_power_vs || + !stateData.power_vs_high_availability + ? false + : stateData.power_vs_zones.length === 0; + }, + }, + power_vs_ha_zone_2: { + size: "small", + type: "select", + labelText: "Power VS Site 2", + hideWhen: hideWhenNotPowerHa, + groups: ["eu-de-1", "eu-de-2", "us-south", "dal10", "dal12"], + onRender: function (stateData) { + return stateData.power_vs_zones[1] || ""; + }, + readOnly: true, + }, power_vs_zones: { size: "small", type: "multiselect", labelText: "Power VS Zones", - hideWhen: hideWhenNotPowerVs, + hideWhen: function (stateData) { + return ( + !stateData.enable_power_vs || stateData.power_vs_high_availability + ); + }, default: [], forceUpdateKey: function (stateData) { return ( @@ -292,6 +351,7 @@ function initOptions(store) { invalid: function (stateData) { return ( stateData.enable_power_vs && + !stateData.power_vs_high_availability && (!stateData.power_vs_zones || isEmpty(stateData.power_vs_zones) || !contains(powerVsZones, stateData.region)) diff --git a/client/src/lib/state/power-vs-instances/power-instances-schema.js b/client/src/lib/state/power-vs-instances/power-instances-schema.js index 0fa8d2fd..74bfde45 100644 --- a/client/src/lib/state/power-vs-instances/power-instances-schema.js +++ b/client/src/lib/state/power-vs-instances/power-instances-schema.js @@ -29,6 +29,23 @@ const { const { sapProfiles, systemTypes } = require("../../constants"); const { nameField } = require("../reusable-fields"); +/** + * hide field for vtl forms when no workspace is selected + * @param {*} vtl + * @returns {Function} hideWhen function + */ +function hideWhenNoWorkspaceAndVtl(vtl) { + return function (stateData, componentProps) { + return ( + vtl && + (isNullOrEmptyString(stateData.workspace, true) || + isEmpty( + componentProps.craig.vtl.image.groups(stateData, componentProps) + )) + ); + }; +} + /** * Network invalidation for powerVs instance * @returns {boolean} function will evaluate to true if should be disabled @@ -52,21 +69,18 @@ function powerVsNetworkInvalid(stateData) { * Processor invalidation for powerVs instance * @returns {boolean} function will evaluate to true if should be disabled */ -function powerVsCoresInvalid(vtl) { - return function (stateData) { - if (stateData.sap) return false; - let isDedicated = stateData.pi_proc_type === "dedicated"; - let coreMax = - stateData.pi_sys_type === "e980" ? 17 : isDedicated ? 13 : 13.75; - let coreMin = isDedicated || vtl ? 1 : 0.25; - let processorsFloat = parseFloat(stateData.pi_processors); - return ( - stateData.pi_processors === "" || - (coreMin === 1 && !isWholeNumber(processorsFloat)) || - (!stateData.sap && - (processorsFloat < coreMin || processorsFloat > coreMax)) - ); - }; +function powerVsCoresInvalid(stateData) { + if (stateData.sap) return false; + let isDedicated = stateData.pi_proc_type === "dedicated"; + let coreMax = + stateData.pi_sys_type === "e980" ? 17 : isDedicated ? 13 : 13.75; + let coreMin = isDedicated ? 1 : 0.25; + let processorsFloat = parseFloat(stateData.pi_processors); + return ( + stateData.pi_processors === "" || + (isDedicated && !isWholeNumber(processorsFloat)) || + (!stateData.sap && (processorsFloat < coreMin || processorsFloat > coreMax)) + ); } /** @@ -92,12 +106,11 @@ function powerVsMemoryInvalid(stateData) { /** * return power_instances processor input invalid text * @param {Object} stateData - * @param {boolean=} vtl * @returns {string} invalid text */ -function invalidPowerVsProcessorTextCallback(stateData, vtl) { +function invalidPowerVsProcessorTextCallback(stateData) { let isDedicated = stateData.pi_proc_type === "dedicated"; - let coreMin = isDedicated || vtl ? 1 : 0.25; + let coreMin = isDedicated ? 1 : 0.25; let coreMax = stateData.pi_sys_type === "e980" ? 17 : isDedicated ? 13 : 13.75; return `Must be a ${ @@ -258,6 +271,7 @@ function powerVsInstanceSchema(vtl) { forceUpdateKey: function (stateData) { return stateData.workspace; }, + hideWhen: hideWhenNoWorkspaceAndVtl(vtl), }, primary_subnet: { labelText: "Primary Subnet", @@ -289,6 +303,7 @@ function powerVsInstanceSchema(vtl) { } else stateData.primary_subnet = ""; }, size: "small", + hideWhen: hideWhenNoWorkspaceAndVtl(vtl), }, ssh_key: { labelText: "SSH Key", @@ -310,6 +325,7 @@ function powerVsInstanceSchema(vtl) { } }, size: "small", + hideWhen: hideWhenNoWorkspaceAndVtl(vtl), }, image: { size: "small", @@ -330,6 +346,7 @@ function powerVsInstanceSchema(vtl) { }); } }, + hideWhen: hideWhenNoWorkspaceAndVtl(vtl), }, pi_sys_type: { size: "small", @@ -339,6 +356,7 @@ function powerVsInstanceSchema(vtl) { invalidText: selectInvalidText("systen type"), groups: vtl ? ["s922", "e980"] : systemTypes, type: "select", + hideWhen: hideWhenNoWorkspaceAndVtl(vtl), }, pi_proc_type: { default: "", @@ -349,21 +367,28 @@ function powerVsInstanceSchema(vtl) { type: "select", onRender: titleCaseRender("pi_proc_type"), onInputChange: kebabCaseInput("pi_proc_type"), + hideWhen: hideWhenNoWorkspaceAndVtl(vtl), }, pi_processors: { labelText: "Processors", placeholder: vtl ? 1 : "0.25", - hideWhen: function (stateData) { - return stateData.sap === true; + hideWhen: function (stateData, componentProps) { + return ( + stateData.sap === true || + hideWhenNoWorkspaceAndVtl(vtl)(stateData, componentProps) + ); }, size: "small", default: "", - invalid: powerVsCoresInvalid(vtl), - invalidText: invalidPowerVsProcessorTextCallback(true), + invalid: powerVsCoresInvalid, + invalidText: invalidPowerVsProcessorTextCallback, }, pi_memory: { - hideWhen: function (stateData) { - return stateData.sap === true; + hideWhen: function (stateData, componentProps) { + return ( + stateData.sap === true || + hideWhenNoWorkspaceAndVtl(vtl)(stateData, componentProps) + ); }, labelText: "Memory (GB)", placeholder: "4", @@ -383,10 +408,14 @@ function powerVsInstanceSchema(vtl) { content: "To attach data volumes from different storage pools, set to false. When this is set to false it cannot be set to true without re-creation of instance.", }, + hideWhen: hideWhenNoWorkspaceAndVtl(vtl), }, pi_storage_pool: powerStoragePoolSelect(), - storage_option: powerVsStorageOptions(), - pi_storage_type: powerVsStorageType(), + storage_option: powerVsStorageOptions( + false, + hideWhenNoWorkspaceAndVtl(vtl) + ), + pi_storage_type: powerVsStorageType(false, hideWhenNoWorkspaceAndVtl(vtl)), affinity_type: powerVsAffinityType(), pi_affinity_policy: { default: null, @@ -406,6 +435,7 @@ function powerVsInstanceSchema(vtl) { : !isWholeNumber(parseInt(stateData.pi_license_repository_capacity)); }, invalidText: unconditionalInvalidText("Enter a whole number"), + hideWhen: hideWhenNoWorkspaceAndVtl(vtl), }, pi_ibmi_css: { size: "small", @@ -443,6 +473,19 @@ function powerVsInstanceSchema(vtl) { labelText: "User Data", placeholder: "Cloud init data", }, + pi_pin_policy: { + labelText: "Pin Policy", + type: "select", + size: "small", + groups: ["Soft", "Hard", "None"], + default: "none", + onRender: titleCaseRender("pi_pin_policy"), + onInputChange: kebabCaseInput("pi_pin_policy"), + tooltip: { + content: + "When you soft pin an instance for high availability, the instance automatically migrates back to the original host once the host is back to its operating state. If the instance has a licensing restriction with the host, the hard pin option restricts the movement of the instance during remote restart, automated remote restart, DRO, and live partition migration. The default pinning policy is none", + }, + }, }; } diff --git a/client/src/lib/state/power-vs-instances/power-vs-instances.js b/client/src/lib/state/power-vs-instances/power-vs-instances.js index 595761be..19c5410c 100644 --- a/client/src/lib/state/power-vs-instances/power-vs-instances.js +++ b/client/src/lib/state/power-vs-instances/power-vs-instances.js @@ -1,4 +1,9 @@ -const { contains, splatContains, getObjectFromArray } = require("lazy-z"); +const { + contains, + splatContains, + getObjectFromArray, + carve, +} = require("lazy-z"); const { shouldDisableComponentSave } = require("../utils"); const { getSapVolumeList } = require("../../forms/sap"); const { RegexButWithWords } = require("regex-but-with-words"); @@ -263,6 +268,34 @@ function powerVsInstanceSave(vtl) { } }); } + config.store.json.power_volumes.forEach((vol) => { + let nextAttachments = []; + vol.attachments.forEach((attachment) => { + if (attachment === componentProps.data.name) { + nextAttachments.push(stateData.name); + } else nextAttachments.push(attachment); + }); + ["pi_affinity_instance", "pi_anti_affinity_instance"].forEach((field) => { + if (vol[field] === componentProps.data.name) { + vol[field] = stateData.name; + } + }); + vol.attachments = nextAttachments; + }); + ["vtl", "power_instances"].forEach((field) => { + config.store.json[field].forEach((instance) => { + if (instance.name !== componentProps.data.name) { + ["pi_affinity_instance", "pi_anti_affinity_instance"].forEach( + (subField) => { + if (instance[subField] === componentProps.data.name) { + instance[subField] = stateData.name; + } + } + ); + } + }); + }); + config.updateChild( ["json", vtl ? "vtl" : "power_instances"], componentProps.data.name, diff --git a/client/src/lib/state/power-vs-volumes.js b/client/src/lib/state/power-vs-volumes.js index 6cd981ad..acb5b184 100644 --- a/client/src/lib/state/power-vs-volumes.js +++ b/client/src/lib/state/power-vs-volumes.js @@ -246,6 +246,9 @@ function initPowerVsVolumeStore(store) { labelText: "Enable Volume Replication", default: false, type: "toggle", + forceUpdateKey: function (stateData) { + return JSON.stringify(stateData); + }, disabled: function (stateData, componentProps) { let pool = stateData.pi_volume_pool; if (!stateData.zone) { diff --git a/client/src/lib/state/reusable-fields.js b/client/src/lib/state/reusable-fields.js index 3bae32d9..ce929358 100644 --- a/client/src/lib/state/reusable-fields.js +++ b/client/src/lib/state/reusable-fields.js @@ -2,15 +2,15 @@ const { contains, allFieldsNull, validPortRange, - containsKeys, splat, isNullOrEmptyString, nestedSplat, revision, transpose, } = require("lazy-z"); -const { newResourceNameExp } = require("../constants"); +const { newResourceNameExp, datacenters } = require("../constants"); const { getAllSecrets } = require("../forms/utils"); +const { RegexButWithWords } = require("regex-but-with-words"); /** * callback function for unconditional invalid text @@ -769,6 +769,127 @@ function networkingRuleCodeField() { }; } +/** + * shortcut for domain field + */ +function domainField() { + return { + default: "", + invalid: function (stateData) { + return ( + // prevent returning error + (stateData.domain || "").match( + new RegexButWithWords() + .stringBegin() + .set("a-z") + .oneOrMore() + .literal(".") + .set("a-z") + .oneOrMore() + .stringEnd() + .done("g") + ) === null + ); + }, + invalidText: unconditionalInvalidText("Enter a valid domain"), + size: "small", + }; +} + +/** + * create field for classic datacenters + * @returns {Object} field object for classic datacenters + */ +function classicDatacenterField() { + return { + default: "", + type: "select", + invalid: fieldIsNullOrEmptyString("datacenter"), + invalidText: selectInvalidText("datacenter"), + onStateChange: function (stateData) { + stateData.private_vlan = ""; + stateData.public_vlan = ""; + }, + groups: datacenters, + size: "small", + }; +} + +/** + * classic vlan filter function + * @param {string} type + * @returns {Function} groups function + */ +function classicVlanFilter(type) { + return function (stateData, componentProps) { + return splat( + componentProps.craig.store.json.classic_vlans.filter((vlan) => { + if (vlan.datacenter === stateData.datacenter && vlan.type === type) + return vlan; + }), + "name" + ); + }; +} + +/** + * create field for classic private vlan + * @returns {Object} field object for classic private vlan + */ +function classicPrivateVlan() { + return { + labelText: "Private VLAN", + default: "", + type: "select", + invalid: fieldIsNullOrEmptyString("private_vlan"), + invalidText: selectInvalidText("private VLAN"), + groups: classicVlanFilter("PRIVATE"), + size: "small", + }; +} + +/** + * create field for classic public vlan + * @returns {Object} field object for classic public vlan + */ +function classicPublicVlan() { + return { + labelText: "Public VLAN", + default: "", + type: "select", + invalid: function (stateData) { + return stateData.private_network_only + ? false + : fieldIsNullOrEmptyString("public_vlan")(stateData); + }, + invalidText: selectInvalidText("public VLAN"), + groups: classicVlanFilter("PUBLIC"), + size: "small", + hideWhen: function (stateData) { + return stateData.private_network_only; + }, + }; +} + +/** + * classic private network toggle + * @returns {Object} field object + */ +function classicPrivateNetworkOnly() { + return { + type: "toggle", + labelText: "Private Network Only", + default: false, + onStateChange: function (stateData) { + if (stateData.private_network_only !== true) { + stateData.private_network_only = true; + stateData.public_vlan = ""; + } else stateData.private_network_only = false; + }, + size: "small", + }; +} + module.exports = { networkingRuleProtocolField, networkingRulePortField, @@ -790,4 +911,9 @@ module.exports = { genericNameCallback, duplicateNameCallback, nameHelperText, + domainField, + classicDatacenterField, + classicPrivateVlan, + classicPublicVlan, + classicPrivateNetworkOnly, }; diff --git a/client/src/lib/state/state.js b/client/src/lib/state/state.js index ef30d4f5..4f1bf07a 100644 --- a/client/src/lib/state/state.js +++ b/client/src/lib/state/state.js @@ -51,6 +51,8 @@ const { initSccV2 } = require("./scc-v2.js"); const { initCisGlbStore } = require("./cis-glb.js"); const { initFortigateStore } = require("./fortigate.js"); const { initClassicSecurityGroups } = require("./classic-security-groups.js"); +const { initClassicVsi } = require("./classic-vsi.js"); +const { initClassicBareMetalStore } = require("./classic-bare-metal.js"); /** * get state for craig @@ -168,6 +170,8 @@ const state = function (legacy) { initCisGlbStore(store); initFortigateStore(store); initClassicSecurityGroups(store); + initClassicVsi(store); + initClassicBareMetalStore(store); /** * hard set config dot json in state store diff --git a/client/src/lib/state/utils.js b/client/src/lib/state/utils.js index c97e0f45..5ddeeb01 100644 --- a/client/src/lib/state/utils.js +++ b/client/src/lib/state/utils.js @@ -27,6 +27,7 @@ const { fieldIsNullOrEmptyString, selectInvalidText, } = require("./reusable-fields"); +const { replicationEnabledStoragePoolMap } = require("../constants"); /** * set kms from encryption key on store update @@ -878,9 +879,10 @@ function powerVsWorkspaceGroups(stateData, componentProps) { /** * get power vs storage options field * @param {boolean=} isVolume true if is volume + * @param {Function=} hideWhen override hidewhen function * @returns {object} schema object */ -function powerVsStorageOptions(isVolume) { +function powerVsStorageOptions(isVolume, hideWhen) { return { size: "small", default: "", @@ -947,6 +949,7 @@ function powerVsStorageOptions(isVolume) { } stateData.affinity_type = null; }, + hideWhen: hideWhen, }; } @@ -955,7 +958,7 @@ function powerVsStorageOptions(isVolume) { * @param {boolean=} isVolume true if is volume * @returns {object} schema object */ -function powerVsStorageType(isVolume) { +function powerVsStorageType(isVolume, hideWhen) { let storageField = isVolume ? "pi_volume_type" : "pi_storage_type"; return { size: "small", @@ -982,8 +985,11 @@ function powerVsStorageType(isVolume) { apiEndpoint: function (stateData) { return `/api/power/${stateData.zone}/storage-tiers`; }, - hideWhen: function (stateData) { - return isNullOrEmptyString(stateData.zone, true); + hideWhen: function (stateData, componentProps) { + return ( + (hideWhen && hideWhen(stateData, componentProps)) || + isNullOrEmptyString(stateData.zone, true) + ); }, }; } @@ -1208,6 +1214,15 @@ function powerStoragePoolSelect(isVolume) { ? "Select a workspace" : selectInvalidText("storage pool")(stateData, componentProps); }, + onInputChange: function (stateData) { + let replicationEnabledPools = + replicationEnabledStoragePoolMap[stateData.zone] || []; + if (!contains(replicationEnabledPools, stateData[field])) { + stateData.pi_replication_enabled = false; + } + + return stateData[field]; + }, apiEndpoint: function (stateData, componentProps) { return `/api/power/${stateData.zone}/storage-pools`; }, diff --git a/client/src/lib/state/vsi.js b/client/src/lib/state/vsi.js index a35c30d7..22de48fa 100644 --- a/client/src/lib/state/vsi.js +++ b/client/src/lib/state/vsi.js @@ -29,7 +29,6 @@ const { encryptionKeyGroups, vpcSshKeyMultiselect, } = require("./utils"); -const { invalidNameText, invalidName } = require("../forms"); const { nameField } = require("./reusable-fields"); /** diff --git a/generate-env.sh b/generate-env.sh index b8ca157d..c43a5c2c 100755 --- a/generate-env.sh +++ b/generate-env.sh @@ -1,6 +1,5 @@ #!/usr/bin/env bash - fatal() { echo "FATAL: $*" exit 1 @@ -8,25 +7,34 @@ fatal() { check_runtime_prereqs() { echo "checking prereqs" - [[ "${BASH_VERSINFO:-0}" -lt 4 ]] && fatal "This script requires bash version 4 or higher. Version ${BASH_VERSINFO} is in use." [[ -z "$(which jq)" ]] && fatal "jq is not installed. See https://stedolan.github.io/jq/" [[ -z "$(which ibmcloud)" ]] && fatal "ibmcloud is not installed" } -install_ibmcloud_plugin () { - [[ -z "$1" ]] && fatal "install_ibmcloud_plugin requries one parameter which contains the name of a plugin to install" - # Install the IBM Cloud plugin - - if ! ibmcloud plugin show $1 &> /dev/null ; then - echo "Installing ibmcloud plugin $1" - ibmcloud plugin install $1 || fatal "Failed to install ibmcloud plugin: $1" +install_ibmcloud_power_iaas_plugin () { + # Install the plugin if is not installed + if ! ibmcloud plugin show power-iaas &> /dev/null ; then + echo "Installing ibmcloud power-iaas plugin" + ibmcloud plugin install power-iaas || fatal "Failed to install the ibmcloud power-iaas plugin" + fi + + # Check the major version of the version and upgrade if it is less than 1 + majorVersion=$(ibmcloud plugin show power-iaas --output json | jq -r '.Version.Major' 2>/dev/null ) + [[ -z "$majorVersion" ]] && fatal "Unable to query the major version of the power-iaas ibmcloud plugin version" + if [[ $majorVersion -lt 1 ]] + then + echo "The power-iaas plugin version is less than 1.0.0, upgrading." + ibmcloud plugin update power-iaas fi } +[[ -z "$1" ]] && fatal "An output file name is required as the first parameter." +ibmcloud iam oauth-tokens &> /dev/null || fatal "Please log in with the ibmcloud CLI (ibmcloud login)" + check_runtime_prereqs -# Install ibmcloud plugins if necessary -install_ibmcloud_plugin power-iaas +# Install or upgrade the power_iaas plugin if necessary +install_ibmcloud_power_iaas_plugin outputfile=$1 echo "# Comment out or remove workspaces that you do not want to use with CRAIG." > $outputfile @@ -36,14 +44,15 @@ echo "" >> $outputfile echo "Fetching workspace information" # Look up all Power VS workspaces and get their name, region (zone), and ID -workspaces=$(ibmcloud pi wss --json | jq -r '.[]? | "\(.name) \(.location.region) \(.id)"') +workspaces=$(ibmcloud pi ws list --json | jq -r '.Payload.workspaces[]? | "\(.name),\(.location.region),\(.id)"') # Cycle through all the workspaces -while read name region id; +while IFS=, read name region id; +while IFS=, read name region id; do echo "# Workspace: ${name}" >> $outputfile - # the ${region^^} makes all characters in the region upper case - echo "POWER_WORKSPACE_${region^^}=$id" >> $outputfile + region_upper=$(echo ${region} | tr '[:lower:]' '[:upper:]') + echo "POWER_WORKSPACE_${region_upper}=$id" >> $outputfile echo "" >> $outputfile done <<< "$workspaces" diff --git a/package-lock.json b/package-lock.json index 00b9f391..8aa7d8ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "craig", - "version": "1.12.1", + "version": "1.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "craig", - "version": "1.12.1", + "version": "1.13.0", "license": "ISC", "dependencies": { "axios": "^1.6.3", diff --git a/package.json b/package.json index 0c4e5d2b..c8275df2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "craig", - "version": "1.12.1", + "version": "1.13.0", "description": "gui for generating ibm cloud infrastructure resources", "main": "index.js", "scripts": { diff --git a/unit-tests/api/craig-api.test.js b/unit-tests/api/craig-api.test.js index c3ad4d6f..07d1322f 100644 --- a/unit-tests/api/craig-api.test.js +++ b/unit-tests/api/craig-api.test.js @@ -103,7 +103,7 @@ describe("craig api", () => { }, { name: "craig/outputs.tf", - data: '##############################################################################\n# Management VPC Outputs\n##############################################################################\n\noutput "management_vpc_name" {\n value = module.management_vpc.name\n}\n\noutput "management_vpc_id" {\n value = module.management_vpc.id\n}\n\noutput "management_vpc_crn" {\n value = module.management_vpc.crn\n}\n\noutput "management_vpc_subnet_vsi_zone_1_name" {\n value = module.management_vpc.vsi_zone_1_name\n}\n\noutput "management_vpc_subnet_vsi_zone_1_id" {\n value = module.management_vpc.vsi_zone_1_id\n}\n\noutput "management_vpc_subnet_vsi_zone_1_crn" {\n value = module.management_vpc.vsi_zone_1_crn\n}\n\noutput "management_vpc_subnet_vpn_zone_1_name" {\n value = module.management_vpc.vpn_zone_1_name\n}\n\noutput "management_vpc_subnet_vpn_zone_1_id" {\n value = module.management_vpc.vpn_zone_1_id\n}\n\noutput "management_vpc_subnet_vpn_zone_1_crn" {\n value = module.management_vpc.vpn_zone_1_crn\n}\n\noutput "management_vpc_subnet_vsi_zone_2_name" {\n value = module.management_vpc.vsi_zone_2_name\n}\n\noutput "management_vpc_subnet_vsi_zone_2_id" {\n value = module.management_vpc.vsi_zone_2_id\n}\n\noutput "management_vpc_subnet_vsi_zone_2_crn" {\n value = module.management_vpc.vsi_zone_2_crn\n}\n\noutput "management_vpc_subnet_vsi_zone_3_name" {\n value = module.management_vpc.vsi_zone_3_name\n}\n\noutput "management_vpc_subnet_vsi_zone_3_id" {\n value = module.management_vpc.vsi_zone_3_id\n}\n\noutput "management_vpc_subnet_vsi_zone_3_crn" {\n value = module.management_vpc.vsi_zone_3_crn\n}\n\noutput "management_vpc_subnet_vpe_zone_1_name" {\n value = module.management_vpc.vpe_zone_1_name\n}\n\noutput "management_vpc_subnet_vpe_zone_1_id" {\n value = module.management_vpc.vpe_zone_1_id\n}\n\noutput "management_vpc_subnet_vpe_zone_1_crn" {\n value = module.management_vpc.vpe_zone_1_crn\n}\n\noutput "management_vpc_subnet_vpe_zone_2_name" {\n value = module.management_vpc.vpe_zone_2_name\n}\n\noutput "management_vpc_subnet_vpe_zone_2_id" {\n value = module.management_vpc.vpe_zone_2_id\n}\n\noutput "management_vpc_subnet_vpe_zone_2_crn" {\n value = module.management_vpc.vpe_zone_2_crn\n}\n\noutput "management_vpc_subnet_vpe_zone_3_name" {\n value = module.management_vpc.vpe_zone_3_name\n}\n\noutput "management_vpc_subnet_vpe_zone_3_id" {\n value = module.management_vpc.vpe_zone_3_id\n}\n\noutput "management_vpc_subnet_vpe_zone_3_crn" {\n value = module.management_vpc.vpe_zone_3_crn\n}\n\noutput "management_vpc_security_group_management_vpe_name" {\n value = module.management_vpc.management_vpe_name\n}\n\noutput "management_vpc_security_group_management_vpe_id" {\n value = module.management_vpc.management_vpe_id\n}\n\noutput "management_vpc_security_group_management_vsi_name" {\n value = module.management_vpc.management_vsi_name\n}\n\noutput "management_vpc_security_group_management_vsi_id" {\n value = module.management_vpc.management_vsi_id\n}\n\n##############################################################################\n\n##############################################################################\n# Workload VPC Outputs\n##############################################################################\n\noutput "workload_vpc_name" {\n value = module.workload_vpc.name\n}\n\noutput "workload_vpc_id" {\n value = module.workload_vpc.id\n}\n\noutput "workload_vpc_crn" {\n value = module.workload_vpc.crn\n}\n\noutput "workload_vpc_subnet_vsi_zone_1_name" {\n value = module.workload_vpc.vsi_zone_1_name\n}\n\noutput "workload_vpc_subnet_vsi_zone_1_id" {\n value = module.workload_vpc.vsi_zone_1_id\n}\n\noutput "workload_vpc_subnet_vsi_zone_1_crn" {\n value = module.workload_vpc.vsi_zone_1_crn\n}\n\noutput "workload_vpc_subnet_vsi_zone_2_name" {\n value = module.workload_vpc.vsi_zone_2_name\n}\n\noutput "workload_vpc_subnet_vsi_zone_2_id" {\n value = module.workload_vpc.vsi_zone_2_id\n}\n\noutput "workload_vpc_subnet_vsi_zone_2_crn" {\n value = module.workload_vpc.vsi_zone_2_crn\n}\n\noutput "workload_vpc_subnet_vsi_zone_3_name" {\n value = module.workload_vpc.vsi_zone_3_name\n}\n\noutput "workload_vpc_subnet_vsi_zone_3_id" {\n value = module.workload_vpc.vsi_zone_3_id\n}\n\noutput "workload_vpc_subnet_vsi_zone_3_crn" {\n value = module.workload_vpc.vsi_zone_3_crn\n}\n\noutput "workload_vpc_subnet_vpe_zone_1_name" {\n value = module.workload_vpc.vpe_zone_1_name\n}\n\noutput "workload_vpc_subnet_vpe_zone_1_id" {\n value = module.workload_vpc.vpe_zone_1_id\n}\n\noutput "workload_vpc_subnet_vpe_zone_1_crn" {\n value = module.workload_vpc.vpe_zone_1_crn\n}\n\noutput "workload_vpc_subnet_vpe_zone_2_name" {\n value = module.workload_vpc.vpe_zone_2_name\n}\n\noutput "workload_vpc_subnet_vpe_zone_2_id" {\n value = module.workload_vpc.vpe_zone_2_id\n}\n\noutput "workload_vpc_subnet_vpe_zone_2_crn" {\n value = module.workload_vpc.vpe_zone_2_crn\n}\n\noutput "workload_vpc_subnet_vpe_zone_3_name" {\n value = module.workload_vpc.vpe_zone_3_name\n}\n\noutput "workload_vpc_subnet_vpe_zone_3_id" {\n value = module.workload_vpc.vpe_zone_3_id\n}\n\noutput "workload_vpc_subnet_vpe_zone_3_crn" {\n value = module.workload_vpc.vpe_zone_3_crn\n}\n\noutput "workload_vpc_security_group_workload_vpe_name" {\n value = module.workload_vpc.workload_vpe_name\n}\n\noutput "workload_vpc_security_group_workload_vpe_id" {\n value = module.workload_vpc.workload_vpe_id\n}\n\n##############################################################################\n\n##############################################################################\n# Management Vpc Management Server Deployment Outputs\n##############################################################################\n\noutput "management_vpc_management_server_vsi_1_1_primary_ip_address" {\n value = ibm_is_instance.management_vpc_management_server_vsi_1_1.primary_network_interface[0].primary_ipv4_address\n}\n\noutput "management_vpc_management_server_vsi_1_2_primary_ip_address" {\n value = ibm_is_instance.management_vpc_management_server_vsi_1_2.primary_network_interface[0].primary_ipv4_address\n}\n\noutput "management_vpc_management_server_vsi_2_1_primary_ip_address" {\n value = ibm_is_instance.management_vpc_management_server_vsi_2_1.primary_network_interface[0].primary_ipv4_address\n}\n\noutput "management_vpc_management_server_vsi_2_2_primary_ip_address" {\n value = ibm_is_instance.management_vpc_management_server_vsi_2_2.primary_network_interface[0].primary_ipv4_address\n}\n\noutput "management_vpc_management_server_vsi_3_1_primary_ip_address" {\n value = ibm_is_instance.management_vpc_management_server_vsi_3_1.primary_network_interface[0].primary_ipv4_address\n}\n\noutput "management_vpc_management_server_vsi_3_2_primary_ip_address" {\n value = ibm_is_instance.management_vpc_management_server_vsi_3_2.primary_network_interface[0].primary_ipv4_address\n}\n\n##############################################################################\n', + data: '##############################################################################\n# Management VPC Outputs\n##############################################################################\n\noutput "management_vpc_name" {\n value = module.management_vpc.name\n}\n\noutput "management_vpc_id" {\n value = module.management_vpc.id\n}\n\noutput "management_vpc_crn" {\n value = module.management_vpc.crn\n}\n\noutput "management_vpc_subnet_vsi_zone_1_name" {\n value = module.management_vpc.vsi_zone_1_name\n}\n\noutput "management_vpc_subnet_vsi_zone_1_id" {\n value = module.management_vpc.vsi_zone_1_id\n}\n\noutput "management_vpc_subnet_vsi_zone_1_crn" {\n value = module.management_vpc.vsi_zone_1_crn\n}\n\noutput "management_vpc_subnet_vpn_zone_1_name" {\n value = module.management_vpc.vpn_zone_1_name\n}\n\noutput "management_vpc_subnet_vpn_zone_1_id" {\n value = module.management_vpc.vpn_zone_1_id\n}\n\noutput "management_vpc_subnet_vpn_zone_1_crn" {\n value = module.management_vpc.vpn_zone_1_crn\n}\n\noutput "management_vpc_subnet_vsi_zone_2_name" {\n value = module.management_vpc.vsi_zone_2_name\n}\n\noutput "management_vpc_subnet_vsi_zone_2_id" {\n value = module.management_vpc.vsi_zone_2_id\n}\n\noutput "management_vpc_subnet_vsi_zone_2_crn" {\n value = module.management_vpc.vsi_zone_2_crn\n}\n\noutput "management_vpc_subnet_vsi_zone_3_name" {\n value = module.management_vpc.vsi_zone_3_name\n}\n\noutput "management_vpc_subnet_vsi_zone_3_id" {\n value = module.management_vpc.vsi_zone_3_id\n}\n\noutput "management_vpc_subnet_vsi_zone_3_crn" {\n value = module.management_vpc.vsi_zone_3_crn\n}\n\noutput "management_vpc_subnet_vpe_zone_1_name" {\n value = module.management_vpc.vpe_zone_1_name\n}\n\noutput "management_vpc_subnet_vpe_zone_1_id" {\n value = module.management_vpc.vpe_zone_1_id\n}\n\noutput "management_vpc_subnet_vpe_zone_1_crn" {\n value = module.management_vpc.vpe_zone_1_crn\n}\n\noutput "management_vpc_subnet_vpe_zone_2_name" {\n value = module.management_vpc.vpe_zone_2_name\n}\n\noutput "management_vpc_subnet_vpe_zone_2_id" {\n value = module.management_vpc.vpe_zone_2_id\n}\n\noutput "management_vpc_subnet_vpe_zone_2_crn" {\n value = module.management_vpc.vpe_zone_2_crn\n}\n\noutput "management_vpc_subnet_vpe_zone_3_name" {\n value = module.management_vpc.vpe_zone_3_name\n}\n\noutput "management_vpc_subnet_vpe_zone_3_id" {\n value = module.management_vpc.vpe_zone_3_id\n}\n\noutput "management_vpc_subnet_vpe_zone_3_crn" {\n value = module.management_vpc.vpe_zone_3_crn\n}\n\noutput "management_vpc_security_group_management_vpe_name" {\n value = module.management_vpc.management_vpe_name\n}\n\noutput "management_vpc_security_group_management_vpe_id" {\n value = module.management_vpc.management_vpe_id\n}\n\noutput "management_vpc_security_group_management_vsi_name" {\n value = module.management_vpc.management_vsi_name\n}\n\noutput "management_vpc_security_group_management_vsi_id" {\n value = module.management_vpc.management_vsi_id\n}\n\n##############################################################################\n\n##############################################################################\n# Workload VPC Outputs\n##############################################################################\n\noutput "workload_vpc_name" {\n value = module.workload_vpc.name\n}\n\noutput "workload_vpc_id" {\n value = module.workload_vpc.id\n}\n\noutput "workload_vpc_crn" {\n value = module.workload_vpc.crn\n}\n\noutput "workload_vpc_subnet_vsi_zone_1_name" {\n value = module.workload_vpc.vsi_zone_1_name\n}\n\noutput "workload_vpc_subnet_vsi_zone_1_id" {\n value = module.workload_vpc.vsi_zone_1_id\n}\n\noutput "workload_vpc_subnet_vsi_zone_1_crn" {\n value = module.workload_vpc.vsi_zone_1_crn\n}\n\noutput "workload_vpc_subnet_vsi_zone_2_name" {\n value = module.workload_vpc.vsi_zone_2_name\n}\n\noutput "workload_vpc_subnet_vsi_zone_2_id" {\n value = module.workload_vpc.vsi_zone_2_id\n}\n\noutput "workload_vpc_subnet_vsi_zone_2_crn" {\n value = module.workload_vpc.vsi_zone_2_crn\n}\n\noutput "workload_vpc_subnet_vsi_zone_3_name" {\n value = module.workload_vpc.vsi_zone_3_name\n}\n\noutput "workload_vpc_subnet_vsi_zone_3_id" {\n value = module.workload_vpc.vsi_zone_3_id\n}\n\noutput "workload_vpc_subnet_vsi_zone_3_crn" {\n value = module.workload_vpc.vsi_zone_3_crn\n}\n\noutput "workload_vpc_subnet_vpe_zone_1_name" {\n value = module.workload_vpc.vpe_zone_1_name\n}\n\noutput "workload_vpc_subnet_vpe_zone_1_id" {\n value = module.workload_vpc.vpe_zone_1_id\n}\n\noutput "workload_vpc_subnet_vpe_zone_1_crn" {\n value = module.workload_vpc.vpe_zone_1_crn\n}\n\noutput "workload_vpc_subnet_vpe_zone_2_name" {\n value = module.workload_vpc.vpe_zone_2_name\n}\n\noutput "workload_vpc_subnet_vpe_zone_2_id" {\n value = module.workload_vpc.vpe_zone_2_id\n}\n\noutput "workload_vpc_subnet_vpe_zone_2_crn" {\n value = module.workload_vpc.vpe_zone_2_crn\n}\n\noutput "workload_vpc_subnet_vpe_zone_3_name" {\n value = module.workload_vpc.vpe_zone_3_name\n}\n\noutput "workload_vpc_subnet_vpe_zone_3_id" {\n value = module.workload_vpc.vpe_zone_3_id\n}\n\noutput "workload_vpc_subnet_vpe_zone_3_crn" {\n value = module.workload_vpc.vpe_zone_3_crn\n}\n\noutput "workload_vpc_security_group_workload_vpe_name" {\n value = module.workload_vpc.workload_vpe_name\n}\n\noutput "workload_vpc_security_group_workload_vpe_id" {\n value = module.workload_vpc.workload_vpe_id\n}\n\n##############################################################################\n\n##############################################################################\n# Management Vpc Management Server Deployment Outputs\n##############################################################################\n\noutput "management_vpc_management_server_vsi_1_1_primary_ip_address" {\n value = ibm_is_instance.management_vpc_management_server_vsi_1_1.primary_network_interface[0].primary_ip[0].address\n}\n\noutput "management_vpc_management_server_vsi_1_2_primary_ip_address" {\n value = ibm_is_instance.management_vpc_management_server_vsi_1_2.primary_network_interface[0].primary_ip[0].address\n}\n\noutput "management_vpc_management_server_vsi_2_1_primary_ip_address" {\n value = ibm_is_instance.management_vpc_management_server_vsi_2_1.primary_network_interface[0].primary_ip[0].address\n}\n\noutput "management_vpc_management_server_vsi_2_2_primary_ip_address" {\n value = ibm_is_instance.management_vpc_management_server_vsi_2_2.primary_network_interface[0].primary_ip[0].address\n}\n\noutput "management_vpc_management_server_vsi_3_1_primary_ip_address" {\n value = ibm_is_instance.management_vpc_management_server_vsi_3_1.primary_network_interface[0].primary_ip[0].address\n}\n\noutput "management_vpc_management_server_vsi_3_2_primary_ip_address" {\n value = ibm_is_instance.management_vpc_management_server_vsi_3_2.primary_network_interface[0].primary_ip[0].address\n}\n\n##############################################################################\n', }, { name: "craig/management_vpc", data: "" }, { name: "craig/management_vpc", data: "" }, diff --git a/unit-tests/data-files/craig-json.json b/unit-tests/data-files/craig-json.json index 46a0aa2d..f0b4b042 100644 --- a/unit-tests/data-files/craig-json.json +++ b/unit-tests/data-files/craig-json.json @@ -955,5 +955,7 @@ }, "cis_glbs": [], "fortigate_vnf": [], - "classic_security_groups": [] + "classic_security_groups": [], + "classic_vsi": [], + "classic_bare_metal": [] } \ No newline at end of file diff --git a/unit-tests/data-files/expected-hard-set.json b/unit-tests/data-files/expected-hard-set.json index 8877efa3..1ddd13ce 100644 --- a/unit-tests/data-files/expected-hard-set.json +++ b/unit-tests/data-files/expected-hard-set.json @@ -1103,5 +1103,7 @@ "resource_group": null },"cis_glbs": [], "fortigate_vnf": [], - "classic_security_groups": [] + "classic_security_groups": [], + "classic_vsi": [], + "classic_bare_metal": [] } \ No newline at end of file diff --git a/unit-tests/data-files/slz.md b/unit-tests/data-files/slz.md index 96fb5322..817ca17d 100644 --- a/unit-tests/data-files/slz.md +++ b/unit-tests/data-files/slz.md @@ -948,4 +948,22 @@ NYI ### Related Links +----- + +## Classic Vsi + +NYI + +### Related Links + + +----- + +## Classic Bare Metal + +NYI + +### Related Links + + ----- diff --git a/unit-tests/forms/components/toggle-form-components.test.js b/unit-tests/forms/components/toggle-form-components.test.js index 86516361..0c49e6d8 100644 --- a/unit-tests/forms/components/toggle-form-components.test.js +++ b/unit-tests/forms/components/toggle-form-components.test.js @@ -107,11 +107,14 @@ describe("toggle form component functions", () => { }); }); describe("dynamicSecondaryButtonProps", () => { - it("should return correct data with no props", () => { + it("should return correct data with no props in CRAIG v2", () => { assert.deepEqual( - dynamicSecondaryButtonProps({ - name: "Resource", - }), + dynamicSecondaryButtonProps( + { + name: "frog", + }, + true + ), { buttonClassName: "cds--btn--danger--tertiary forceTertiaryButtonStyles", @@ -124,12 +127,35 @@ describe("toggle form component functions", () => { "it should return correct data" ); }); + it("should return correct data with no props in Classic CRAIG", () => { + assert.deepEqual( + dynamicSecondaryButtonProps( + { + name: "frog", + }, + false + ), + { + buttonClassName: + "cds--btn--danger--tertiary forceTertiaryButtonStyles", + iconClassName: "redFill", + popoverProps: { + className: "", + hoverText: "Delete frog", + }, + }, + "it should return correct data" + ); + }); it("should return correct data when disabled", () => { assert.deepEqual( - dynamicSecondaryButtonProps({ - disabled: true, - name: "Resource", - }), + dynamicSecondaryButtonProps( + { + disabled: true, + name: "Resource", + }, + false + ), { buttonClassName: "cds--btn--danger--tertiary forceTertiaryButtonStyles pointerEventsNone", @@ -144,10 +170,13 @@ describe("toggle form component functions", () => { }); it("should return correct data when disabled and disabled delete message", () => { assert.deepEqual( - dynamicSecondaryButtonProps({ - disabled: true, - disableDeleteMessage: "no", - }), + dynamicSecondaryButtonProps( + { + disabled: true, + disableDeleteMessage: "no", + }, + false + ), { buttonClassName: "cds--btn--danger--tertiary forceTertiaryButtonStyles pointerEventsNone", diff --git a/unit-tests/forms/disable-save/classic-gateways.test.js b/unit-tests/forms/disable-save/classic-gateways.test.js index df65254f..ce26d435 100644 --- a/unit-tests/forms/disable-save/classic-gateways.test.js +++ b/unit-tests/forms/disable-save/classic-gateways.test.js @@ -168,6 +168,7 @@ describe("classic_gateways", () => { ); }); it("should return true if a gw has no private_vlan", () => { + craig.store.json.classic_vlans = []; assert.isTrue( disableSave( "classic_gateways", diff --git a/unit-tests/forms/dynamic-form-fields/toggle.test.js b/unit-tests/forms/dynamic-form-fields/toggle.test.js index 959b516c..284e327e 100644 --- a/unit-tests/forms/dynamic-form-fields/toggle.test.js +++ b/unit-tests/forms/dynamic-form-fields/toggle.test.js @@ -39,6 +39,10 @@ describe("dynamic toggle", () => { }); it("should return props form properly formatted toggle", () => { let toggleData; + let keyData; + let forceUpdateKey = function () { + return "foo"; + }; let actualData = dynamicToggleProps({ parentProps: {}, parentState: {}, @@ -49,6 +53,7 @@ describe("dynamic toggle", () => { return false; }, labelText: "Use Data", + forceUpdateKey: forceUpdateKey, }, handleInputChange: function (name) { toggleData = name; @@ -62,6 +67,7 @@ describe("dynamic toggle", () => { labelB: "True", labelText: "Use Data", disabled: false, + key: "foo", }; assert.isFunction(actualData.onToggle, "it should be a function"); actualData.onToggle(); @@ -70,6 +76,11 @@ describe("dynamic toggle", () => { "use_data", "it should return name to parent function" ); + assert.deepEqual( + actualData.key, + "foo", + "it should return name to parent function" + ); delete actualData.onToggle; assert.deepEqual( actualData, @@ -102,6 +113,7 @@ describe("dynamic toggle", () => { labelB: "CRAIG Managed Network Addresses", labelText: "VPC Network Address Management", disabled: false, + key: undefined, }; assert.isFunction(actualData.onToggle, "it should be a function"); actualData.onToggle(); @@ -145,6 +157,7 @@ describe("dynamic toggle", () => { labelB: "True", labelText: "Use Data", disabled: false, + key: undefined, }; assert.isFunction(actualData.onToggle, "it should be a function"); actualData.onToggle(); @@ -186,6 +199,7 @@ describe("dynamic toggle", () => { labelB: "True", labelText: " ", disabled: false, + key: undefined, }; assert.isFunction(actualData.onToggle, "it should be a function"); actualData.onToggle(); @@ -227,6 +241,7 @@ describe("dynamic toggle", () => { labelB: "True", labelText: " ", disabled: false, + key: undefined, }; assert.isFunction(actualData.onToggle, "it should be a function"); actualData.onToggle(); @@ -269,6 +284,7 @@ describe("dynamic toggle", () => { labelB: "On", labelText: " ", disabled: false, + key: undefined, }; assert.isFunction(actualData.onToggle, "it should be a function"); actualData.onToggle(); diff --git a/unit-tests/forms/wizard.test.js b/unit-tests/forms/wizard.test.js index b622555e..ea37c62b 100644 --- a/unit-tests/forms/wizard.test.js +++ b/unit-tests/forms/wizard.test.js @@ -57,7 +57,7 @@ describe("setup wizard", () => { enable_power_vs: false, enable_classic: false, power_vs_zones: [], - craig_version: "1.12.2", + craig_version: "1.13.0", power_vs_high_availability: false, no_vpn_secrets_manager_auth: false, }, @@ -907,7 +907,9 @@ describe("setup wizard", () => { power_volumes: [], classic_ssh_keys: [], classic_vlans: [], + classic_vsi: [], classic_security_groups: [], + classic_bare_metal: [], classic_gateways: [], cis: [], vtl: [], @@ -973,7 +975,7 @@ describe("setup wizard", () => { enable_power_vs: false, enable_classic: false, power_vs_zones: [], - craig_version: "1.12.2", + craig_version: "1.13.0", power_vs_high_availability: false, no_vpn_secrets_manager_auth: false, }, @@ -1823,7 +1825,9 @@ describe("setup wizard", () => { power_volumes: [], classic_ssh_keys: [], classic_vlans: [], + classic_vsi: [], classic_security_groups: [], + classic_bare_metal: [], classic_gateways: [], cis: [], vtl: [], @@ -1888,7 +1892,7 @@ describe("setup wizard", () => { enable_power_vs: false, enable_classic: false, power_vs_zones: [], - craig_version: "1.12.2", + craig_version: "1.13.0", power_vs_high_availability: false, no_vpn_secrets_manager_auth: false, }, @@ -2332,7 +2336,9 @@ describe("setup wizard", () => { power_volumes: [], classic_ssh_keys: [], classic_vlans: [], + classic_vsi: [], classic_security_groups: [], + classic_bare_metal: [], classic_gateways: [], cis: [], vtl: [], @@ -2397,7 +2403,7 @@ describe("setup wizard", () => { enable_power_vs: false, enable_classic: false, power_vs_zones: [], - craig_version: "1.12.2", + craig_version: "1.13.0", power_vs_high_availability: false, no_vpn_secrets_manager_auth: false, }, @@ -2832,7 +2838,9 @@ describe("setup wizard", () => { power_volumes: [], classic_ssh_keys: [], classic_vlans: [], + classic_vsi: [], classic_security_groups: [], + classic_bare_metal: [], classic_gateways: [], cis: [], vtl: [], @@ -2897,7 +2905,7 @@ describe("setup wizard", () => { enable_power_vs: false, enable_classic: false, power_vs_zones: [], - craig_version: "1.12.2", + craig_version: "1.13.0", power_vs_high_availability: false, no_vpn_secrets_manager_auth: false, }, @@ -3408,7 +3416,9 @@ describe("setup wizard", () => { power_volumes: [], classic_ssh_keys: [], classic_vlans: [], + classic_vsi: [], classic_security_groups: [], + classic_bare_metal: [], classic_gateways: [], cis: [], vtl: [], @@ -3474,7 +3484,7 @@ describe("setup wizard", () => { enable_power_vs: false, enable_classic: false, power_vs_zones: [], - craig_version: "1.12.2", + craig_version: "1.13.0", no_vpn_secrets_manager_auth: false, }, resource_groups: [ @@ -3577,7 +3587,9 @@ describe("setup wizard", () => { power_volumes: [], classic_ssh_keys: [], classic_vlans: [], + classic_vsi: [], classic_security_groups: [], + classic_bare_metal: [], classic_gateways: [], cis: [], vtl: [], @@ -3642,7 +3654,7 @@ describe("setup wizard", () => { enable_power_vs: false, enable_classic: false, power_vs_zones: [], - craig_version: "1.12.2", + craig_version: "1.13.0", power_vs_high_availability: false, no_vpn_secrets_manager_auth: false, }, @@ -4146,7 +4158,9 @@ describe("setup wizard", () => { power_volumes: [], classic_ssh_keys: [], classic_vlans: [], + classic_vsi: [], classic_security_groups: [], + classic_bare_metal: [], classic_gateways: [], cis: [], vtl: [], @@ -4211,7 +4225,7 @@ describe("setup wizard", () => { enable_power_vs: false, enable_classic: false, power_vs_zones: [], - craig_version: "1.12.2", + craig_version: "1.13.0", power_vs_high_availability: false, no_vpn_secrets_manager_auth: false, }, @@ -4690,7 +4704,9 @@ describe("setup wizard", () => { power_volumes: [], classic_ssh_keys: [], classic_vlans: [], + classic_vsi: [], classic_security_groups: [], + classic_bare_metal: [], classic_gateways: [], cis: [], vtl: [], @@ -4754,7 +4770,7 @@ describe("setup wizard", () => { enable_power_vs: false, enable_classic: false, power_vs_zones: [], - craig_version: "1.12.2", + craig_version: "1.13.0", power_vs_high_availability: false, no_vpn_secrets_manager_auth: false, }, @@ -5237,7 +5253,9 @@ describe("setup wizard", () => { power_volumes: [], classic_ssh_keys: [], classic_vlans: [], + classic_vsi: [], classic_security_groups: [], + classic_bare_metal: [], classic_gateways: [], cis: [], vtl: [], @@ -5302,7 +5320,7 @@ describe("setup wizard", () => { enable_classic: false, enable_classic: false, power_vs_zones: [], - craig_version: "1.12.2", + craig_version: "1.13.0", no_vpn_secrets_manager_auth: false, }, resource_groups: [ @@ -5728,7 +5746,9 @@ describe("setup wizard", () => { power_volumes: [], classic_ssh_keys: [], classic_vlans: [], + classic_vsi: [], classic_security_groups: [], + classic_bare_metal: [], classic_gateways: [], cis: [], vtl: [], @@ -5792,7 +5812,7 @@ describe("setup wizard", () => { enable_power_vs: true, enable_classic: false, power_vs_zones: ["dal10"], - craig_version: "1.12.2", + craig_version: "1.13.0", power_vs_high_availability: false, no_vpn_secrets_manager_auth: false, }, @@ -6219,7 +6239,9 @@ describe("setup wizard", () => { power_volumes: [], classic_ssh_keys: [], classic_vlans: [], + classic_vsi: [], classic_security_groups: [], + classic_bare_metal: [], classic_gateways: [], cis: [], vtl: [], @@ -6292,7 +6314,7 @@ describe("setup wizard", () => { enable_power_vs: true, enable_classic: false, power_vs_zones: ["dal10"], - craig_version: "1.12.2", + craig_version: "1.13.0", power_vs_high_availability: false, no_vpn_secrets_manager_auth: false, }, @@ -7533,7 +7555,9 @@ describe("setup wizard", () => { power_volumes: [], classic_ssh_keys: [], classic_vlans: [], + classic_vsi: [], classic_security_groups: [], + classic_bare_metal: [], classic_gateways: [], cis: [], vtl: [], diff --git a/unit-tests/json-to-iac/classic-bare-metal.test.js b/unit-tests/json-to-iac/classic-bare-metal.test.js new file mode 100644 index 00000000..e5c054c0 --- /dev/null +++ b/unit-tests/json-to-iac/classic-bare-metal.test.js @@ -0,0 +1,157 @@ +const { assert } = require("chai"); +const { + formatClassicBareMetal, + classicBareMetalTf, +} = require("../../client/src/lib/json-to-iac/classic-bare-metal"); + +describe("classic bare metal", () => { + describe("formatClassicBareMetal", () => { + it("should format a classic bare metal server", () => { + let actualData = formatClassicBareMetal( + { + package_key_name: "test", + process_key_name: "test", + os_key_name: "test", + memory: "256", + name: "name", + datacenter: "dal10", + domain: "example.com", + network_speed: "100", + public_bandwidth: "500", + private_network_only: false, + private_vlan: "priv", + public_vlan: "pub", + disk_key_names: ["key-1", "key-2"], + }, + { + _options: { + tags: ["hello", "world"], + }, + } + ); + let expectedData = ` +resource "ibm_compute_bare_metal" "name" { + package_key_name = "test" + process_key_name = "test" + os_key_name = "test" + memory = "256" + hostname = "\${var.prefix}-name" + domain = "example.com" + datacenter = "dal10" + network_speed = "100" + public_bandwidth = "500" + hourly_billing = false + private_network_only = false + private_vlan_id = ibm_network_vlan.classic_vlan_priv.id + public_vlan_id = ibm_network_vlan.classic_vlan_pub.id + disk_key_names = [ + "key-1", + "key-2" + ] +} +`; + assert.deepEqual( + actualData, + expectedData, + "it should return correct bare metal tf" + ); + }); + }); + describe("classicBareMetalTf", () => { + it("should format a classic bare metal server", () => { + let actualData = classicBareMetalTf({ + classic_bare_metal: [ + { + name: "test", + domain: "test.com", + datacenter: "dal10", + os_key_name: "frog", + package_key_name: "frog", + process_key_name: "frog", + private_network_only: false, + private_vlan: "private-vlan", + public_vlan: "public-vlan", + disk_key_names: ["disk-key-1", "disk-key-2"], + }, + ], + }); + let expectedData = `############################################################################## +# Classic Bare Metal Servers +############################################################################## + +resource "ibm_compute_bare_metal" "test" { + package_key_name = "frog" + process_key_name = "frog" + os_key_name = "frog" + memory = 64 + hostname = "\${var.prefix}-test" + domain = "test.com" + datacenter = "dal10" + network_speed = 100 + public_bandwidth = 500 + hourly_billing = false + private_network_only = false + private_vlan_id = ibm_network_vlan.classic_vlan_private_vlan.id + public_vlan_id = ibm_network_vlan.classic_vlan_public_vlan.id + disk_key_names = [ + "disk-key-1", + "disk-key-2" + ] +} + +############################################################################## +`; + assert.deepEqual( + actualData, + expectedData, + "it should return correct bare metal tf" + ); + }); + it("should format a bare metal server", () => { + let actualData = classicBareMetalTf({ + classic_bare_metal: [ + { + name: "test", + domain: "test.com", + datacenter: "dal10", + os_key_name: "frog", + package_key_name: "frog", + process_key_name: "frog", + private_network_only: true, + private_vlan: "private-vlan", + disk_key_names: ["disk-key-1", "disk-key-2"], + }, + ], + }); + let expectedData = `############################################################################## +# Classic Bare Metal Servers +############################################################################## + +resource "ibm_compute_bare_metal" "test" { + package_key_name = "frog" + process_key_name = "frog" + os_key_name = "frog" + memory = 64 + hostname = "\${var.prefix}-test" + domain = "test.com" + datacenter = "dal10" + network_speed = 100 + hourly_billing = false + private_network_only = true + private_vlan_id = ibm_network_vlan.classic_vlan_private_vlan.id + disk_key_names = [ + "disk-key-1", + "disk-key-2" + ] +} + +############################################################################## +`; + assert.deepEqual( + actualData, + expectedData, + "it should return correct bare metal tf" + ); + }); + }); +}); diff --git a/unit-tests/json-to-iac/classic-vsi.test.js b/unit-tests/json-to-iac/classic-vsi.test.js new file mode 100644 index 00000000..9ec127d5 --- /dev/null +++ b/unit-tests/json-to-iac/classic-vsi.test.js @@ -0,0 +1,172 @@ +const { assert } = require("chai"); +const { + formatClassicVsi, + classicVsiTf, +} = require("../../client/src/lib/json-to-iac/classic-vsi"); + +describe("classic vsi", () => { + describe("formatClassicVsi", () => { + it("should format a classic vsi", () => { + let actualData = formatClassicVsi( + { + name: "name", + datacenter: "dal10", + domain: "example.com", + cores: "12", + memory: "256", + image_id: "xyz1234", + local_disk: true, + network_speed: "100", + private_network_only: false, + private_vlan: "example-classic-private", + public_vlan: "example-classic-public", + private_security_groups: ["priv-sg"], + public_security_groups: ["pub-sg"], + ssh_keys: ["example-classic"], + }, + { + _options: { + tags: ["hello", "world"], + }, + } + ); + let expectedData = ` +resource "ibm_compute_vm_instance" "classic_vsi_name" { + hostname = "\${var.prefix}-name" + datacenter = "dal10" + domain = "example.com" + cores = 12 + memory = 256 + image_id = "xyz1234" + local_disk = true + network_speed = 100 + private_network_only = false + private_vlan_id = ibm_network_vlan.classic_vlan_example_classic_private.id + public_vlan_id = ibm_network_vlan.classic_vlan_example_classic_public.id + public_security_group_ids = [ + ibm_security_group.classic_securtiy_group_pub_sg.id + ] + private_security_group_ids = [ + ibm_security_group.classic_securtiy_group_priv_sg.id + ] + ssh_key_ids = [ + ibm_compute_ssh_key.classic_ssh_key_example_classic.id + ] + tags = [ + "hello", + "world" + ] +} +`; + assert.deepEqual( + actualData, + expectedData, + "it should return correct vsi" + ); + }); + }); + describe("classicVsiTf", () => { + it("should format a classic vsi", () => { + let actualData = classicVsiTf({ + _options: { + tags: ["hello", "world"], + }, + classic_vsi: [ + { + name: "name", + datacenter: "dal10", + domain: "example.com", + cores: "12", + memory: "256", + image_id: "xyz1234", + local_disk: true, + network_speed: "100", + private_network_only: false, + private_vlan: "example-classic-private", + public_vlan: "example-classic-public", + private_security_groups: ["priv-sg"], + public_security_groups: ["pub-sg"], + ssh_keys: ["example-classic"], + }, + { + name: "name2", + datacenter: "dal10", + domain: "example.com", + cores: "12", + memory: "256", + image_id: "xyz1234", + local_disk: true, + network_speed: "100", + private_network_only: true, + private_vlan: "example-classic-private", + public_vlan: "example-classic-public", + private_security_groups: ["priv-sg"], + public_security_groups: ["pub-sg"], + ssh_keys: ["example-classic"], + }, + ], + }); + let expectedData = `############################################################################## +# Classic VSI +############################################################################## + +resource "ibm_compute_vm_instance" "classic_vsi_name" { + hostname = "\${var.prefix}-name" + datacenter = "dal10" + domain = "example.com" + cores = 12 + memory = 256 + image_id = "xyz1234" + local_disk = true + network_speed = 100 + private_network_only = false + private_vlan_id = ibm_network_vlan.classic_vlan_example_classic_private.id + public_vlan_id = ibm_network_vlan.classic_vlan_example_classic_public.id + public_security_group_ids = [ + ibm_security_group.classic_securtiy_group_pub_sg.id + ] + private_security_group_ids = [ + ibm_security_group.classic_securtiy_group_priv_sg.id + ] + ssh_key_ids = [ + ibm_compute_ssh_key.classic_ssh_key_example_classic.id + ] + tags = [ + "hello", + "world" + ] +} + +resource "ibm_compute_vm_instance" "classic_vsi_name2" { + hostname = "\${var.prefix}-name2" + datacenter = "dal10" + domain = "example.com" + cores = 12 + memory = 256 + image_id = "xyz1234" + local_disk = true + network_speed = 100 + private_network_only = true + private_vlan_id = ibm_network_vlan.classic_vlan_example_classic_private.id + private_security_group_ids = [ + ibm_security_group.classic_securtiy_group_priv_sg.id + ] + ssh_key_ids = [ + ibm_compute_ssh_key.classic_ssh_key_example_classic.id + ] + tags = [ + "hello", + "world" + ] +} + +############################################################################## +`; + assert.deepEqual( + actualData, + expectedData, + "it should return correct vsi" + ); + }); + }); +}); diff --git a/unit-tests/json-to-iac/outputs.test.js b/unit-tests/json-to-iac/outputs.test.js index f04756e8..00a34525 100644 --- a/unit-tests/json-to-iac/outputs.test.js +++ b/unit-tests/json-to-iac/outputs.test.js @@ -6,7 +6,7 @@ describe("outputs", () => { it("should return correct outputs file", () => { let config = { _options: { - craig_version: "1.12.1", + craig_version: "1.13.0", prefix: "jv-dev", region: "eu-de", tags: ["hello", "world"], @@ -581,7 +581,7 @@ output "management_vpc_security_group_management_vsi_id" { ############################################################################## output "management_vpc_jv_dev_server_vsi_1_1_primary_ip_address" { - value = ibm_is_instance.management_vpc_jv_dev_server_vsi_1_1.primary_network_interface[0].primary_ipv4_address + value = ibm_is_instance.management_vpc_jv_dev_server_vsi_1_1.primary_network_interface[0].primary_ip[0].address } output "management_vpc_jv_dev_server_vsi_1_1_floating_ip_address" { @@ -599,7 +599,7 @@ output "management_vpc_jv_dev_server_vsi_1_1_floating_ip_address" { it("should return correct outputs file with multiple deployments", () => { let config = { _options: { - craig_version: "1.12.1", + craig_version: "1.13.0", prefix: "jv-dev", region: "eu-de", tags: ["hello", "world"], @@ -1195,7 +1195,7 @@ output "management_vpc_security_group_management_vsi_id" { ############################################################################## output "management_vpc_jv_dev_server_vsi_1_1_primary_ip_address" { - value = ibm_is_instance.management_vpc_jv_dev_server_vsi_1_1.primary_network_interface[0].primary_ipv4_address + value = ibm_is_instance.management_vpc_jv_dev_server_vsi_1_1.primary_network_interface[0].primary_ip[0].address } output "management_vpc_jv_dev_server_vsi_1_1_floating_ip_address" { @@ -1209,7 +1209,7 @@ output "management_vpc_jv_dev_server_vsi_1_1_floating_ip_address" { ############################################################################## output "management_vpc_jv_dev_server2_vsi_1_1_primary_ip_address" { - value = ibm_is_instance.management_vpc_jv_dev_server2_vsi_1_1.primary_network_interface[0].primary_ipv4_address + value = ibm_is_instance.management_vpc_jv_dev_server2_vsi_1_1.primary_network_interface[0].primary_ip[0].address } output "management_vpc_jv_dev_server2_vsi_1_1_floating_ip_address" { @@ -1237,7 +1237,7 @@ output "management_vpc_jv_dev_server2_vsi_1_1_floating_ip_address" { enable_classic: false, dynamic_subnets: true, enable_power_vs: true, - craig_version: "1.12.2", + craig_version: "1.13.0", power_vs_zones: ["us-south", "dal10", "dal12"], power_vs_high_availability: false, no_vpn_secrets_manager_auth: false, @@ -1463,7 +1463,7 @@ output "power_vs_workspace_iac_power_workspace_test_output_crn" { it("should return correct outputs for power vs workspaces and vpc", () => { let config = { _options: { - craig_version: "1.12.1", + craig_version: "1.13.0", prefix: "jv-dev", region: "eu-de", tags: ["hello", "world"], @@ -2317,7 +2317,7 @@ output "management2_vpc_subnet_vsi_zone_1_crn" { ############################################################################## output "management2_vpc_jv_dev_server2_vsi_1_1_primary_ip_address" { - value = ibm_is_instance.management2_vpc_jv_dev_server2_vsi_1_1.primary_network_interface[0].primary_ipv4_address + value = ibm_is_instance.management2_vpc_jv_dev_server2_vsi_1_1.primary_network_interface[0].primary_ip[0].address } output "management2_vpc_jv_dev_server2_vsi_1_1_floating_ip_address" { @@ -2368,5 +2368,257 @@ output "power_vs_workspace_iac_power_workspace_test_output_crn" { "it should return correct outputs" ); }); + it("should return the correct outputs for power vs instances", () => { + let actualData = outputsTf({ + _options: { + prefix: "jvdev", + region: "us-south", + tags: ["hello", "world"], + zones: 3, + endpoints: "private", + account_id: null, + fs_cloud: false, + enable_classic: false, + dynamic_subnets: true, + enable_power_vs: true, + craig_version: "1.13.0", + power_vs_zones: ["dal10"], + power_vs_high_availability: false, + no_vpn_secrets_manager_auth: false, + template: "Empty Project", + power_vs_ha_zone_1: null, + power_vs_ha_zone_2: null, + }, + access_groups: [], + appid: [], + atracker: { + enabled: false, + type: "cos", + name: "atracker", + target_name: "atracker-cos", + bucket: null, + add_route: true, + cos_key: null, + locations: ["global", "us-south"], + instance: false, + plan: "lite", + resource_group: null, + }, + cbr_rules: [], + cbr_zones: [], + clusters: [], + dns: [], + event_streams: [], + f5_vsi: [], + iam_account_settings: { + enable: false, + mfa: null, + allowed_ip_addresses: null, + include_history: false, + if_match: null, + max_sessions_per_identity: null, + restrict_create_service_id: null, + restrict_create_platform_apikey: null, + session_expiration_in_seconds: null, + session_invalidation_in_seconds: null, + }, + icd: [], + key_management: [], + load_balancers: [], + logdna: { + name: "logdna", + archive: false, + enabled: false, + plan: "lite", + endpoints: "private", + platform_logs: false, + resource_group: null, + cos: null, + bucket: null, + }, + object_storage: [], + power: [ + { + use_data: false, + name: "vsi", + zone: "dal10", + resource_group: "asset-development", + imageNames: ["CentOS-Stream-8"], + images: [ + { + creationDate: "2023-09-20T22:15:08.000Z", + description: "", + href: "/pcloud/v1/cloud-instances/d839ff9f75e2465a81707aa69ee9a9b7/stock-images/ecb9553c-9b7d-4a53-bf0c-0ab2c748bbc7", + imageID: "ecb9553c-9b7d-4a53-bf0c-0ab2c748bbc7", + lastUpdateDate: "2023-09-21T09:35:17.000Z", + name: "CentOS-Stream-8", + specifications: { + architecture: "ppc64", + containerFormat: "bare", + diskFormat: "raw", + endianness: "little-endian", + hypervisorType: "phyp", + operatingSystem: "rhel", + }, + state: "active", + storagePool: "Tier3-Flash-1", + storageType: "tier3", + workspace: "vsi", + zone: "dal10", + workspace_use_data: false, + }, + ], + ssh_keys: [ + { + workspace_use_data: false, + use_data: false, + name: "vsi", + public_key: "NONE", + workspace: "vsi", + zone: "dal10", + }, + ], + network: [ + { + workspace_use_data: false, + name: "nw", + use_data: false, + pi_network_type: "vlan", + pi_cidr: "10.10.10.10/25", + pi_dns: ["127.0.0.1"], + pi_network_mtu: "1450", + workspace: "vsi", + zone: "dal10", + }, + ], + cloud_connections: [], + attachments: [], + }, + ], + power_instances: [ + { + sap: false, + sap_profile: null, + name: "output-test", + ssh_key: "vsi", + workspace: "vsi", + network: [ + { + name: "nw", + ip_address: "", + }, + ], + primary_subnet: "nw", + image: "CentOS-Stream-8", + pi_sys_type: "e880", + pi_storage_pool_affinity: false, + pi_proc_type: "shared", + pi_processors: "0.25", + pi_memory: "2", + pi_ibmi_css: false, + pi_ibmi_pha: false, + pi_ibmi_rds_users: null, + pi_storage_type: "tier1", + storage_option: "None", + pi_storage_pool: null, + affinity_type: null, + pi_affinity_volume: null, + pi_anti_affinity_volume: null, + pi_anti_affinity_instance: null, + pi_affinity_instance: null, + pi_user_data: null, + zone: "dal10", + pi_affinity_policy: null, + }, + ], + power_volumes: [], + resource_groups: [ + { + use_prefix: false, + name: "asset-development", + use_data: true, + }, + ], + routing_tables: [], + scc: { + credential_description: null, + id: null, + passphrase: null, + name: "", + location: "us", + collector_description: null, + is_public: false, + scope_description: null, + enable: false, + }, + secrets_manager: [], + security_groups: [], + ssh_keys: [], + sysdig: { + enabled: false, + plan: "graduated-tier", + resource_group: null, + name: "sysdig", + platform_logs: false, + }, + teleport_vsi: [], + transit_gateways: [], + virtual_private_endpoints: [], + vpcs: [], + vpn_gateways: [], + vpn_servers: [], + vsi: [], + classic_ssh_keys: [], + classic_vlans: [], + vtl: [], + classic_gateways: [], + cis: [], + scc_v2: { + enable: false, + resource_group: null, + region: "", + account_id: "${var.account_id}", + profile_attachments: [], + }, + cis_glbs: [], + fortigate_vnf: [], + classic_security_groups: [], + classic_vsi: [], + classic_bare_metal: [], + }); + let expectedData = `############################################################################## +# VSI Power Workspace Outputs +############################################################################## + +output "power_vs_workspace_vsi_name" { + value = ibm_resource_instance.power_vs_workspace_vsi.name +} + +output "power_vs_workspace_vsi_guid" { + value = ibm_resource_instance.power_vs_workspace_vsi.guid +} + +output "power_vs_workspace_vsi_crn" { + value = ibm_resource_instance.power_vs_workspace_vsi.crn +} + +############################################################################## + +############################################################################## +# Power VS Instance Outputs +############################################################################## + +output "vsi_workspace_instance_output_test_primary_ip" { + value = ibm_pi_instance.vsi_workspace_instance_output_test.pi_network[0].ip_address +} + +############################################################################## +`; + assert.deepEqual( + actualData, + expectedData, + "it should create correct outputs" + ); + }); }); }); diff --git a/unit-tests/json-to-iac/power-vs-instances.test.js b/unit-tests/json-to-iac/power-vs-instances.test.js index edadab2d..91201de7 100644 --- a/unit-tests/json-to-iac/power-vs-instances.test.js +++ b/unit-tests/json-to-iac/power-vs-instances.test.js @@ -47,6 +47,53 @@ resource "ibm_pi_instance" "example_workspace_instance_test" { network_id = ibm_pi_network.power_network_example_dev_nw.network_id } } +`; + assert.deepEqual( + actualData, + expectedData, + "it should return correct instance data" + ); + }); + it("should correctly return power vs instance data with pin policy", () => { + let actualData = formatPowerVsInstance({ + zone: "dal12", + workspace: "example", + name: "test", + image: "SLES15-SP3-SAP", + ssh_key: "keyname", + network: [ + { + name: "dev-nw", + }, + ], + primary_subnet: "dev-nw", + pi_memory: "4", + pi_processors: "2", + pi_proc_type: "shared", + pi_sys_type: "s922", + pi_health_status: "WARNING", + pi_storage_type: "tier1", + pi_user_data: "", + pi_pin_policy: "soft", + }); + let expectedData = ` +resource "ibm_pi_instance" "example_workspace_instance_test" { + provider = ibm.power_vs_dal12 + pi_image_id = ibm_pi_image.power_image_example_sles15_sp3_sap.image_id + pi_key_pair_name = ibm_pi_key.power_vs_ssh_key_keyname.pi_key_name + pi_cloud_instance_id = ibm_resource_instance.power_vs_workspace_example.guid + pi_instance_name = "\${var.prefix}-test" + pi_memory = "4" + pi_processors = "2" + pi_proc_type = "shared" + pi_sys_type = "s922" + pi_health_status = "WARNING" + pi_storage_type = "tier1" + pi_pin_policy = "soft" + pi_network { + network_id = ibm_pi_network.power_network_example_dev_nw.network_id + } +} `; assert.deepEqual( actualData, diff --git a/unit-tests/json-to-iac/variables.test.js b/unit-tests/json-to-iac/variables.test.js index 6b2b6a70..303e806b 100644 --- a/unit-tests/json-to-iac/variables.test.js +++ b/unit-tests/json-to-iac/variables.test.js @@ -928,7 +928,7 @@ variable "secrets_manager_example_secret_password" { enable_power_vs: true, enable_classic: false, power_vs_zones: ["dal10"], - craig_version: "1.12.2", + craig_version: "1.13.0", power_vs_high_availability: false, template: "Empty Project", fs_cloud: false, diff --git a/unit-tests/state/classic-bare-metal.test.js b/unit-tests/state/classic-bare-metal.test.js new file mode 100644 index 00000000..36dc5219 --- /dev/null +++ b/unit-tests/state/classic-bare-metal.test.js @@ -0,0 +1,155 @@ +const { assert } = require("chai"); +const { state } = require("../../client/src/lib/state"); +const { disableSave } = require("../../client/src/lib"); + +/** + * initialize store + * @returns {lazyZState} state store + */ +function newState() { + let store = new state(); + store.setUpdateCallback(() => {}); + return store; +} + +describe("classic bare metal state", () => { + let craig; + beforeEach(() => { + craig = newState(); + }); + describe("classic_bare_metal.init", () => { + it("should initialize classic bare metal servers", () => { + assert.deepEqual( + craig.store.json.classic_bare_metal, + [], + "it should initialize data" + ); + }); + }); + describe("clasic_bare_metal.create", () => { + it("should create a bare metal instance", () => { + craig.classic_bare_metal.create({ name: "test", domain: "test.com" }); + assert.deepEqual( + craig.store.json.classic_bare_metal, + [ + { + name: "test", + domain: "test.com", + }, + ], + "it should return correct data" + ); + }); + }); + describe("classic_bare_metal.save", () => { + it("should update a bare metal instance", () => { + craig.classic_bare_metal.create({ name: "test", domain: "test.com" }); + craig.classic_bare_metal.save( + { name: "frog", domain: "frog.com" }, + { + craig: craig, + data: { + name: "test", + }, + } + ); + assert.deepEqual( + craig.store.json.classic_bare_metal, + [ + { + name: "frog", + domain: "frog.com", + }, + ], + "it should return correct data" + ); + }); + }); + describe("classic_bare_metal.delete", () => { + it("should delete a bare metal instance", () => { + craig.classic_bare_metal.create({ name: "test", domain: "test.com" }); + craig.classic_bare_metal.delete( + {}, + { data: { name: "test", domain: "test.com" } } + ); + assert.deepEqual( + craig.store.json.classic_bare_metal, + [], + "it should return correct data" + ); + }); + }); + describe("classic_bare_metal.schema", () => { + it("save should be disabled when empty", () => { + assert.isTrue( + disableSave("classic_bare_metal", {}, { craig: craig }), + "it should be disabled" + ); + }); + it("should return return true if name is invalid", () => { + assert.isTrue( + craig.classic_bare_metal.name.invalid({ + name: "---", + }), + "it should return true" + ); + }); + it("should return return true if domain is invalid", () => { + assert.isTrue( + craig.classic_bare_metal.domain.invalid({ + domain: "frog", + }), + "it should return true" + ); + }); + it("should return return true if os_key_name is empty", () => { + assert.isTrue( + craig.classic_bare_metal.os_key_name.invalid({ + os_key_name: "", + }), + "it should return true" + ); + }); + it("should return return true if package_key_name is empty", () => { + assert.isTrue( + craig.classic_bare_metal.package_key_name.invalid({ + package_key_name: "", + }), + "it should return true" + ); + }); + it("should return return true if process_key_name is empty", () => { + assert.isTrue( + craig.classic_bare_metal.process_key_name.invalid({ + process_key_name: "", + }), + "it should return true" + ); + }); + it("should return return true if disk_key_names is empty", () => { + assert.isTrue( + craig.classic_bare_metal.disk_key_names.invalid({ + disk_key_names: [], + }), + "it should return true" + ); + }); + it("should hide public_bandwidth if private_network_only is true", () => { + assert.isTrue( + craig.classic_bare_metal.public_bandwidth.hideWhen({ + private_network_only: true, + }), + "it should return true" + ); + }); + it("should return true if private_network_only is false and bandwidth is empty", () => { + assert.isTrue( + craig.classic_bare_metal.public_bandwidth.invalid({ + private_network_only: false, + public_bandwidth: "", + }), + "it should return true" + ); + }); + }); +}); diff --git a/unit-tests/state/classic-vsi.test.js b/unit-tests/state/classic-vsi.test.js new file mode 100644 index 00000000..d43ca3b3 --- /dev/null +++ b/unit-tests/state/classic-vsi.test.js @@ -0,0 +1,202 @@ +const { assert } = require("chai"); +const { state } = require("../../client/src/lib/state"); +const { disableSave } = require("../../client/src/lib"); + +/** + * initialize store + * @returns {lazyZState} state store + */ +function newState() { + let store = new state(); + store.setUpdateCallback(() => {}); + return store; +} + +describe("classic vsi state", () => { + let craig; + beforeEach(() => { + craig = newState(); + }); + describe("crud ops", () => { + beforeEach(() => { + craig.classic_ssh_keys.create({ + name: "key", + public_key: "1234", + datacenter: "dal10", + }); + craig.classic_security_groups.create({ name: "pub", description: "" }); + craig.classic_security_groups.create({ name: "priv", description: "" }); + craig.classic_vlans.create({ + name: "pub", + datacenter: "dal10", + type: "PUBLIC", + }); + craig.classic_vlans.create({ + name: "priv", + datacenter: "dal10", + type: "PRIVATE", + }); + }); + it("should create a vsi and set unfound values to null", () => { + craig.classic_vsi.create({ + name: "test", + ssh_keys: ["aaa"], + public_vlan: "aa", + private_vlan: "aa", + private_security_groups: ["aaa"], + public_security_groups: ["aaa"], + }); + assert.deepEqual( + craig.store.json.classic_vsi, + [ + { + name: "test", + public_vlan: null, + private_vlan: null, + private_security_groups: [], + public_security_groups: [], + ssh_keys: [], + }, + ], + "it should create vsi" + ); + }); + it("should create a vsi with found values", () => { + craig.classic_vsi.create({ + name: "test", + ssh_keys: ["key"], + public_vlan: "pub", + private_vlan: "priv", + private_security_groups: ["pub"], + public_security_groups: ["priv"], + }); + assert.deepEqual( + craig.store.json.classic_vsi, + [ + { + name: "test", + public_vlan: "pub", + private_vlan: "priv", + private_security_groups: ["pub"], + public_security_groups: ["priv"], + ssh_keys: ["key"], + }, + ], + "it should create vsi" + ); + craig.classic_vsi.save( + { + name: "honk", + ssh_keys: ["key"], + public_vlan: "pub", + private_vlan: "priv", + private_security_groups: ["pub"], + public_security_groups: ["priv"], + }, + { + data: { + name: "test", + }, + } + ); + assert.deepEqual( + craig.store.json.classic_vsi, + [ + { + name: "honk", + public_vlan: "pub", + private_vlan: "priv", + private_security_groups: ["pub"], + public_security_groups: ["priv"], + ssh_keys: ["key"], + }, + ], + "it should update vsi" + ); + craig.classic_vsi.delete( + { + name: "honk", + ssh_keys: ["key"], + public_vlan: "pub", + private_vlan: "priv", + private_security_groups: ["pub"], + public_security_groups: ["priv"], + }, + { + data: { + name: "honk", + }, + } + ); + assert.deepEqual( + craig.store.json.classic_vsi, + [], + "it should delete vsi" + ); + }); + }); + describe("schema", () => { + it("should be invalid when no values", () => { + assert.isTrue( + disableSave("classic_vsi", {}, { craig: craig }), + "it should be disabled" + ); + }); + it("should return invalid when no ssh keys selected", () => { + assert.isTrue( + craig.classic_vsi.ssh_keys.invalid({ ssh_keys: [] }), + "it should be invalid" + ); + }); + it("should return groups for classic ssh keys", () => { + assert.deepEqual( + craig.classic_vsi.ssh_keys.groups({}, { craig: craig }), + [], + "it should return data" + ); + }); + it("should return invalid and groups for private security groups", () => { + assert.isTrue( + craig.classic_vsi.private_security_groups.invalid({ + private_security_groups: [], + }), + "it should be invalid" + ); + assert.deepEqual( + craig.classic_vsi.private_security_groups.groups({}, { craig: craig }), + [], + "it should return data" + ); + }); + it("should return invalid and groups for public security groups", () => { + assert.isTrue( + craig.classic_vsi.public_security_groups.invalid({ + private_security_groups: [], + public_security_groups: [], + }), + "it should be invalid" + ); + assert.isFalse( + craig.classic_vsi.public_security_groups.invalid({ + private_security_groups: [], + private_network_only: true, + public_security_groups: [], + }), + "it should be valid when none and private only" + ); + assert.isTrue( + craig.classic_vsi.public_security_groups.hideWhen({ + private_security_groups: [], + private_network_only: true, + public_security_groups: [], + }), + "it should be hidden" + ); + assert.deepEqual( + craig.classic_vsi.public_security_groups.groups({}, { craig: craig }), + [], + "it should return data" + ); + }); + }); +}); diff --git a/unit-tests/state/options.test.js b/unit-tests/state/options.test.js index 56c9025e..f5ebc5bb 100644 --- a/unit-tests/state/options.test.js +++ b/unit-tests/state/options.test.js @@ -272,6 +272,7 @@ describe("options", () => { data, { enable_power_vs: true, + power_vs_zones: [], }, "it should change value" ); @@ -295,7 +296,7 @@ describe("options", () => { data, { power_vs_high_availability: true, - power_vs_zones: ["dal12", "wdc06"], + power_vs_zones: [], }, "it should change value" ); @@ -373,6 +374,19 @@ describe("options", () => { "it should return value" ); }); + it("should hide power vs zones", () => { + assert.isTrue( + craig.options.power_vs_zones.hideWhen({}), + "it should be hidden" + ); + assert.isTrue( + craig.options.power_vs_zones.hideWhen({ + enable_power_vs: true, + power_vs_high_availability: true, + }), + "it should be hidden" + ); + }); it("should return correct power_vs_zones groups when power_vs_high_availability true", () => { assert.deepEqual( craig.options.power_vs_zones.groups({ @@ -408,6 +422,71 @@ describe("options", () => { "it should return empty array" ); }); + it("should hide power_vs_ha_zone_1 when not ha", () => { + assert.isTrue( + craig.options.power_vs_ha_zone_1.hideWhen({ + enable_power_vs: true, + power_vs_high_availability: false, + }), + "it should be hidden" + ); + }); + it("should return correct value for power_vs_ha_zone_1 on render", () => { + assert.deepEqual( + craig.options.power_vs_ha_zone_1.onRender({ power_vs_zones: [] }), + "", + "it should return correct data" + ); + assert.deepEqual( + craig.options.power_vs_ha_zone_2.onRender({ power_vs_zones: [] }), + "", + "it should return correct data" + ); + assert.deepEqual( + craig.options.power_vs_ha_zone_1.onRender({ + power_vs_zones: ["mad02", "eu-de-1"], + }), + "mad02", + "it should return correct data" + ); + assert.deepEqual( + craig.options.power_vs_ha_zone_2.onRender({ + power_vs_zones: ["mad02", "eu-de-1"], + }), + "eu-de-1", + "it should return correct data" + ); + }); + it("should change power vs zones when changing power_vs_ha_zone_1", () => { + let data = { + power_vs_ha_zone_1: "mad02", + power_vs_zones: [], + }; + craig.options.power_vs_ha_zone_1.onStateChange(data); + assert.deepEqual( + data.power_vs_zones, + ["mad02", "eu-de-1"], + "it should set zones" + ); + }); + it("should have correct invalid for ha zone 1", () => { + assert.isFalse( + craig.options.power_vs_ha_zone_1.invalid({}), + "it should be false when no power vs" + ); + assert.isFalse( + craig.options.power_vs_ha_zone_1.invalid({ enable_power_vs: true }), + "it should be false when no power vs ha" + ); + assert.isTrue( + craig.options.power_vs_ha_zone_1.invalid({ + enable_power_vs: true, + power_vs_high_availability: true, + power_vs_zones: [], + }), + "it should be true when no power vs zones" + ); + }); }); }); }); diff --git a/unit-tests/state/power-vs-instances.test.js b/unit-tests/state/power-vs-instances.test.js index 8983c1eb..86e0350d 100644 --- a/unit-tests/state/power-vs-instances.test.js +++ b/unit-tests/state/power-vs-instances.test.js @@ -1214,6 +1214,87 @@ describe("power_instances", () => { "it should create correct volumes" ); }); + it("should update volume reference when changing a power vs instance name", () => { + let state = newState(); + state.store.json._options.power_vs_zones = ["dal12", "dal10"]; + state.power.create({ + name: "toad", + images: [{ name: "7100-05-09", workspace: "toad" }], + zone: "dal12", + }); + state.power_instances.create({ + name: "frog", + sap: false, + zone: "dal12", + workspace: "toad", + network: [], + storage_option: "Affinity", + pi_affinity_volume: "ignore-me", + }); + state.power_instances.create({ + name: "honk", + sap: false, + zone: "dal12", + workspace: "toad", + network: [], + storage_option: "Anti-Affinity", + pi_affinity_instance: "frog", + }); + state.vtl.create({ + name: "boop", + sap: false, + zone: "dal12", + workspace: "toad", + network: [], + storage_option: "Anti-Affinity", + pi_anti_affinity_instance: "frog", + }); + state.power_volumes.create({ + attachments: ["frog"], + workspace: "toad", + name: "ignore-me", + pi_anti_affinity_instance: "frog", + }); + state.power_volumes.create({ + attachments: ["honk", "beep"], + workspace: "toad", + name: "ignore-me2", + pi_affinity_instance: "frog", + }); + state.power_instances.save( + { name: "toad" }, + { + data: { + name: "frog", + }, + } + ); + assert.deepEqual( + state.store.json.power_volumes[0].attachments, + ["toad"], + "it should update name" + ); + assert.deepEqual( + state.store.json.power_volumes[0].pi_anti_affinity_instance, + "toad", + "it should update name" + ); + assert.deepEqual( + state.store.json.power_volumes[1].pi_affinity_instance, + "toad", + "it should update name" + ); + assert.deepEqual( + state.store.json.power_instances[1].pi_affinity_instance, + "toad", + "it should update name" + ); + assert.deepEqual( + state.store.json.vtl[0].pi_anti_affinity_instance, + "toad", + "it should update name" + ); + }); }); describe("power_instances.delete", () => { it("should delete a power vs instance", () => { diff --git a/unit-tests/state/power-vs-volumes.test.js b/unit-tests/state/power-vs-volumes.test.js index cc85fe4d..2e15361b 100644 --- a/unit-tests/state/power-vs-volumes.test.js +++ b/unit-tests/state/power-vs-volumes.test.js @@ -360,6 +360,26 @@ describe("power_volumes", () => { "it should return correct invalid text" ); }); + it("should not disable replication on input change when zone is replication enabled", () => { + let data = { + zone: "dal10", + pi_volume_pool: "General-Flash-53", + pi_replication_enabled: true, + }; + let actualData = craig.power_volumes.pi_volume_pool.onInputChange(data); + assert.deepEqual(actualData, "General-Flash-53", "should return pool"); + assert.isTrue(data.pi_replication_enabled, "should be true"); + }); + it("should disable replication on input change when zone is not replication enabled", () => { + let data = { + zone: "eu-es", + pi_volume_pool: "General-Flash-53", + pi_replication_enabled: true, + }; + let actualData = craig.power_volumes.pi_volume_pool.onInputChange(data); + assert.deepEqual(actualData, "General-Flash-53", "should return pool"); + assert.isFalse(data.pi_replication_enabled, "should be false"); + }); it("should update state on workspace change", () => { let actualData = {}; craig.power_volumes.workspace.onStateChange( @@ -619,11 +639,22 @@ describe("power_volumes", () => { it("should be true for when the workspace's zone does not have replication enabled", () => { let data = { pi_volume_pool: "Tier1-Flash-8", - zone: "dal10", + zone: "eu-es", }; assert.isTrue( craig.power_volumes.pi_replication_enabled.disabled(data, {}) ); }); + it("should return correct JSON string from forceUpdateKey", () => { + let data = { + foo: "bar", + }; + let expectedData = '{"foo":"bar"}'; + assert.deepEqual( + craig.power_volumes.pi_replication_enabled.forceUpdateKey(data), + expectedData, + "should be equal" + ); + }); }); }); diff --git a/unit-tests/state/vtl.test.js b/unit-tests/state/vtl.test.js index 90ad0fb9..4766f4bb 100644 --- a/unit-tests/state/vtl.test.js +++ b/unit-tests/state/vtl.test.js @@ -1285,6 +1285,55 @@ describe("vtl", () => { beforeEach(() => { craig = newState(); }); + describe("vtl.pi_proc_type.hideWhen", () => { + it("should hide when no workspace is selected or no vtl images", () => { + assert.isTrue( + craig.vtl.pi_proc_type.hideWhen({}, {}), + "it should be hidden" + ); + craig.power.create({ name: "bad" }); + assert.isTrue( + craig.vtl.pi_proc_type.hideWhen( + { workspace: "bad" }, + { craig: craig } + ), + "it should be hidden" + ); + assert.isTrue( + craig.vtl.pi_processors.hideWhen( + { workspace: "bad" }, + { craig: craig } + ), + "it should be hidden" + ); + assert.isTrue( + craig.vtl.pi_memory.hideWhen({ workspace: "bad" }, { craig: craig }), + "it should be hidden" + ); + assert.isTrue( + craig.vtl.storage_option.hideWhen( + { workspace: "bad" }, + { craig: craig } + ), + "it should be hidden" + ); + assert.isTrue( + craig.vtl.storage_option.hideWhen( + { workspace: "bad", zone: "" }, + { craig: craig } + ), + "it should be hidden" + ); + craig.store.json.power[0].imageNames = ["VTL"]; + assert.isFalse( + craig.vtl.storage_option.hideWhen( + { workspace: "bad", zone: "yes" }, + { craig: craig } + ), + "it should not be hidden" + ); + }); + }); describe("vtl.pi_license_repository_capacity.invalid", () => { it("should return true when empty string", () => { assert.isTrue(