diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f73bbd3b..b86daaf5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,8 +2,8 @@ image: ${ARTIFACTORY_SERVER}/dockerhub-remote/node:8 stages: - check_content - - build_package - test + - build_package - test_functional_init - test_functional_execute - test_functional_cleanup @@ -68,28 +68,6 @@ check_content: tags: - cm-official-docker-executor -build_package: - image: ${ARTIFACTORY_SERVER}/dockerhub-remote/f5devcentral/containthedocs:rpmbuild - stage: build_package - except: - variables: - - $CI_COMMIT_REF_NAME == "docs_production" - - $CI_COMMIT_REF_NAME == "docs_staging" - script: - - echo 'CI BUILD' - # install packages: jq - - apt-get update - - apt-get install -y jq - # build RPM, handles dependency installation, etc. - - bash ./scripts/build_rpm.sh - tags: - - cm-official-docker-executor - artifacts: - name: f5-cloud-failover-$CI_BUILD_REF - paths: - - dist - expire_in: 1 month - # test package test_package: stage: test @@ -145,6 +123,28 @@ coverage: - coverage expire_in: 1 month +build_package: + image: ${ARTIFACTORY_SERVER}/dockerhub-remote/f5devcentral/containthedocs:rpmbuild + stage: build_package + except: + variables: + - $CI_COMMIT_REF_NAME == "docs_production" + - $CI_COMMIT_REF_NAME == "docs_staging" + script: + - echo 'CI BUILD' + # install packages: jq + - apt-get update + - apt-get install -y jq + # build RPM, handles dependency installation, etc. + - bash ./scripts/build_rpm.sh + tags: + - cm-official-docker-executor + artifacts: + name: f5-cloud-failover-$CI_BUILD_REF + paths: + - dist + expire_in: 1 month + ### Functional Tests Section # Functional Tests - Initialization phase (with 1 retries in a case of any failures) @@ -195,6 +195,8 @@ test_functional_init_azure: except: variables: - $TESTS_TIER == "2" + - $CF_ENV_CLOUD == "aws" + - $CF_ENV_CLOUD == "gcp" # init functional tests: azure - 1nic test_functional_init_azure_1nic: @@ -206,6 +208,8 @@ test_functional_init_azure_1nic: except: variables: - $TESTS_TIER == "1" + - $CF_ENV_CLOUD == "aws" + - $CF_ENV_CLOUD == "gcp" - $CI_COMMIT_MESSAGE =~ /smart:run_functional_tests/ # init functional tests: aws - across network topology @@ -218,6 +222,8 @@ test_functional_init_aws_across_net: except: variables: - $TESTS_TIER == "2" + - $CF_ENV_CLOUD == "azure" + - $CF_ENV_CLOUD == "gcp" # init functional tests: aws - across network topology for 1nic test_functional_init_aws_across_net_1nic: @@ -230,6 +236,8 @@ test_functional_init_aws_across_net_1nic: except: variables: - $TESTS_TIER == "1" + - $CF_ENV_CLOUD == "azure" + - $CF_ENV_CLOUD == "gcp" - $CI_COMMIT_MESSAGE =~ /smart:run_functional_tests/ # init functional tests: aws - same network topology in us-west-2 @@ -242,6 +250,8 @@ test_functional_init_aws: except: variables: - $TESTS_TIER == "1" + - $CF_ENV_CLOUD == "azure" + - $CF_ENV_CLOUD == "gcp" - $CI_COMMIT_MESSAGE =~ /smart:run_functional_tests/ # init functional tests: aws - same network topology in us-east-1 @@ -252,6 +262,10 @@ test_functional_init_aws_east: CF_ENV_USE_AVAILABILITY_ZONES: "false" CF_ENV_REGION: "us-east-1" when: manual + except: + variables: + - $CF_ENV_CLOUD == "azure" + - $CF_ENV_CLOUD == "gcp" # init functional tests: aws - same network topology in ca-central-1 test_functional_init_aws_ca_central: @@ -261,6 +275,10 @@ test_functional_init_aws_ca_central: CF_ENV_USE_AVAILABILITY_ZONES: "false" CF_ENV_REGION: "ca-central-1" when: manual + except: + variables: + - $CF_ENV_CLOUD == "azure" + - $CF_ENV_CLOUD == "gcp" # init functional tests: aws - same network topology for 1nic test_functional_init_aws_1nic: @@ -273,6 +291,8 @@ test_functional_init_aws_1nic: except: variables: - $TESTS_TIER == "1" + - $CF_ENV_CLOUD == "azure" + - $CF_ENV_CLOUD == "gcp" - $CI_COMMIT_MESSAGE =~ /smart:run_functional_tests/ # init functional tests: gcp @@ -284,6 +304,8 @@ test_functional_init_gcp: except: variables: - $TESTS_TIER == "2" + - $CF_ENV_CLOUD == "azure" + - $CF_ENV_CLOUD == "aws" # init functional tests: gcp no forwarding test_functional_init_gcp_no_forwarding: @@ -295,6 +317,8 @@ test_functional_init_gcp_no_forwarding: except: variables: - $TESTS_TIER == "2" + - $CF_ENV_CLOUD == "azure" + - $CF_ENV_CLOUD == "aws" # Functional Tests - Execute phase (with no retries) .test_functional_execute_generic: &test_functional_execute_generic @@ -349,6 +373,8 @@ test_functional_execute_azure: except: variables: - $TESTS_TIER == "2" + - $CF_ENV_CLOUD == "aws" + - $CF_ENV_CLOUD == "gcp" # run functional tests: azure 1 nic test_functional_execute_azure_1nic: @@ -366,6 +392,8 @@ test_functional_execute_azure_1nic: except: variables: - $TESTS_TIER == "1" + - $CF_ENV_CLOUD == "aws" + - $CF_ENV_CLOUD == "gcp" - $CI_COMMIT_MESSAGE =~ /smart:run_functional_tests/ # run functional tests: aws - across network topology @@ -384,6 +412,8 @@ test_functional_execute_aws_across_net: except: variables: - $TESTS_TIER == "2" + - $CF_ENV_CLOUD == "azure" + - $CF_ENV_CLOUD == "gcp" # run functional tests: aws - across network topology for 1nic test_functional_execute_aws_across_net_1nic: @@ -402,6 +432,8 @@ test_functional_execute_aws_across_net_1nic: except: variables: - $TESTS_TIER == "1" + - $CF_ENV_CLOUD == "azure" + - $CF_ENV_CLOUD == "gcp" - $CI_COMMIT_MESSAGE =~ /smart:run_functional_tests/ # run functional tests: aws - same network topology @@ -420,6 +452,8 @@ test_functional_execute_aws: except: variables: - $TESTS_TIER == "1" + - $CF_ENV_CLOUD == "azure" + - $CF_ENV_CLOUD == "gcp" - $CI_COMMIT_MESSAGE =~ /smart:run_functional_tests/ # run functional tests: aws - same network topology - us-east-1 @@ -436,6 +470,10 @@ test_functional_execute_aws_east: - test_functional_init_aws_east - build_package when: manual + except: + variables: + - $CF_ENV_CLOUD == "azure" + - $CF_ENV_CLOUD == "gcp" # run functional tests: aws - same network topology - ca-central-1 test_functional_execute_aws_ca_central: @@ -451,6 +489,10 @@ test_functional_execute_aws_ca_central: - test_functional_init_aws_ca_central - build_package when: manual + except: + variables: + - $CF_ENV_CLOUD == "azure" + - $CF_ENV_CLOUD == "gcp" # run functional tests: aws - same network topology for 1nic test_functional_execute_aws_1nic: @@ -469,6 +511,8 @@ test_functional_execute_aws_1nic: except: variables: - $TESTS_TIER == "1" + - $CF_ENV_CLOUD == "azure" + - $CF_ENV_CLOUD == "gcp" - $CI_COMMIT_MESSAGE =~ /smart:run_functional_tests/ # run functional tests: gcp with forwarding rule @@ -486,6 +530,8 @@ test_functional_execute_gcp: except: variables: - $TESTS_TIER == "2" + - $CF_ENV_CLOUD == "azure" + - $CF_ENV_CLOUD == "aws" # run functional tests: gcp without forwarding rules test_functional_execute_gcp_no_forwarding: @@ -502,6 +548,8 @@ test_functional_execute_gcp_no_forwarding: except: variables: - $TESTS_TIER == "2" + - $CF_ENV_CLOUD == "azure" + - $CF_ENV_CLOUD == "aws" # Functional Tests - Cleanup phase (executes always with 1 retry in a case of any failures) .test_functional_cleanup_generic: &test_functional_cleanup_generic @@ -554,6 +602,8 @@ test_functional_cleanup_azure: except: variables: - $TESTS_TIER == "2" + - $CF_ENV_CLOUD == "aws" + - $CF_ENV_CLOUD == "gcp" # run functional tests: azure_1nic test_functional_cleanup_azure_1nic: @@ -571,6 +621,8 @@ test_functional_cleanup_azure_1nic: except: variables: - $TESTS_TIER == "1" + - $CF_ENV_CLOUD == "aws" + - $CF_ENV_CLOUD == "gcp" - $CI_COMMIT_MESSAGE =~ /smart:run_functional_tests/ # run functional tests: aws - across network topology @@ -589,6 +641,8 @@ test_functional_cleanup_aws_across_net: except: variables: - $TESTS_TIER == "2" + - $CF_ENV_CLOUD == "azure" + - $CF_ENV_CLOUD == "gcp" # run functional tests: aws - across network topology for 1nic test_functional_cleanup_aws_across_net_1nic: @@ -607,6 +661,8 @@ test_functional_cleanup_aws_across_net_1nic: except: variables: - $TESTS_TIER == "1" + - $CF_ENV_CLOUD == "azure" + - $CF_ENV_CLOUD == "gcp" - $CI_COMMIT_MESSAGE =~ /smart:run_functional_tests/ # run functional tests: aws - same network topology @@ -625,6 +681,8 @@ test_functional_cleanup_aws: except: variables: - $TESTS_TIER == "1" + - $CF_ENV_CLOUD == "azure" + - $CF_ENV_CLOUD == "gcp" - $CI_COMMIT_MESSAGE =~ /smart:run_functional_tests/ # run functional tests: aws - same network topology - us-east-1 @@ -641,6 +699,10 @@ test_functional_cleanup_aws_east: - test_functional_init_aws_east - test_functional_execute_aws_east when: manual + except: + variables: + - $CF_ENV_CLOUD == "azure" + - $CF_ENV_CLOUD == "gcp" # run functional tests: aws - same network topology - ca-central-1 test_functional_cleanup_aws_ca_central: @@ -656,6 +718,10 @@ test_functional_cleanup_aws_ca_central: - test_functional_init_aws_ca_central - test_functional_execute_aws_ca_central when: manual + except: + variables: + - $CF_ENV_CLOUD == "azure" + - $CF_ENV_CLOUD == "gcp" # run functional tests: aws - same network topology for 1nic test_functional_cleanup_aws_1nic: @@ -674,6 +740,8 @@ test_functional_cleanup_aws_1nic: except: variables: - $TESTS_TIER == "1" + - $CF_ENV_CLOUD == "azure" + - $CF_ENV_CLOUD == "gcp" - $CI_COMMIT_MESSAGE =~ /smart:run_functional_tests/ # run functional tests: gcp @@ -691,6 +759,8 @@ test_functional_cleanup_gcp: except: variables: - $TESTS_TIER == "2" + - $CF_ENV_CLOUD == "azure" + - $CF_ENV_CLOUD == "aws" # run functional tests: gcp no forwarding rule test_functional_cleanup_gcp_no_forwarding: @@ -708,6 +778,8 @@ test_functional_cleanup_gcp_no_forwarding: except: variables: - $TESTS_TIER == "2" + - $CF_ENV_CLOUD == "azure" + - $CF_ENV_CLOUD == "aws" ### End of Functional Tests diff --git a/deployment-tool b/deployment-tool index fda6f259..24eca466 160000 --- a/deployment-tool +++ b/deployment-tool @@ -1 +1 @@ -Subproject commit fda6f2596d5faba7a962cc0f21ae142f742519d6 +Subproject commit 24eca4665fa7ac877cd8aa9c9dae97c99bcc41df diff --git a/docs/userguide/aws-same-az.rst b/docs/userguide/aws-same-az.rst index 15b8da22..8960629f 100644 --- a/docs/userguide/aws-same-az.rst +++ b/docs/userguide/aws-same-az.rst @@ -79,13 +79,13 @@ Example AWS Declaration ----------------------- This example declaration shows the minimum information needed to update the cloud resources in AWS. See the :ref:`quickstart` section for steps on how to post this declaration. See the :ref:`example-declarations` section for more examples. -.. literalinclude:: ../../examples/declarations/aws-same-az-1.7.0.json +.. literalinclude:: ../../examples/declarations/aws-same-az-1.13.0.json :language: json :caption: Example AWS Declaration with Single Routing Table :tab-width: 4 :linenos: -:fonticon:`fa fa-download` :download:`aws-same-az.json <../../examples/declarations/aws-same-az-1.7.0.json>` +:fonticon:`fa fa-download` :download:`aws-same-az.json <../../examples/declarations/aws-same-az-1.13.0.json>` | @@ -125,8 +125,12 @@ In order to successfully implement CFE in AWS, you need an AWS Identity and Acce s3:ListAllMyBuckets \* Current Account externalStorage To discover (using scopingTags) bucket used for failover state file. s3:ListBucket S3 Bucket ID Optional externalStorage To return information about a bucket. s3:PutObject S3 Bucket ID/Key Optional externalStorage To write failover state file. + kms:DescribeKey KMS Encryption Key ID Optional externalStorage To write failover state file when using a **customer managed** KMS key for server-side encryption. + kms:GenerateDataKey KMS Encryption Key ID Optional externalStorage To write failover state file when using a **customer managed** KMS key for server-side encryption. + kms:Decrypt KMS Encryption Key ID Optional externalStorage To write failover state file when using a **customer managed** KMS key for server-side encryption. ======================================== ============================== ======================= ======================= ===================================================================================================================== - | + + | For example, to create a role for an EC2 service follow these steps: @@ -150,7 +154,7 @@ In order to successfully implement CFE in AWS, you need an AWS Identity and Acce | -#. Assign an IAM role to each instance by navigating to **EC2 > Instances > Instance > Actions > Instance Settings > Attach/Replace IAM Role** +2. Assign an IAM role to each instance by navigating to **EC2 > Instances > Instance > Actions > Instance Settings > Attach/Replace IAM Role** For example: @@ -161,8 +165,6 @@ In order to successfully implement CFE in AWS, you need an AWS Identity and Acce .. _aws-same-az-iam-example: -.. _aws-iam-example: - IAM Role Example Declaration ```````````````````````````` Below is an example F5 policy that includes IAM roles. @@ -172,7 +174,7 @@ Below is an example F5 policy that includes IAM roles. .. code-block:: json - { + { "BigIpHighAvailabilityAccessRole": { "Condition": "failover", "Type": "AWS::IAM::Role", @@ -230,6 +232,26 @@ Below is an example F5 policy that includes IAM roles. ], "Resource": "arn:*:s3:::/*" }, + { + "Action": [ + "s3:PutObject" + ], + "Condition": { + "Null": { + "s3:x-amz-server-side-encryption": true + } + }, + "Effect": "Deny", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:*:s3:::/*" + ] + ] + }, + "Sid": "DenyPublishingUnencryptedResources" + }, { "Effect": "Allow", "Action": [ @@ -319,12 +341,30 @@ Below is an example F5 policy that includes IAM roles. ] } } - } + } + +| + +NOTE: If a customer managed KMS Encryption Key is used for server-side encryption on the S3 bucket, the following permissions are required: + +.. code-block:: json + + { + "Effect": "Allow", + "Action": [ + "kms:DescribeKey", + "kms:GenerateDataKey", + "kms:Decrypt" + ], + "Resource": "arn:aws:kms:::key/" + }, | +To limit encryption to a specific type or for more information, see `AWS Documentation `_. + -Alternatively, for *Actions* that **do** allow resource level permissions, but the specific resource IDs may not be known ahead of time, you can leverage *Condition* statements that limit access to only those resources with a certain tag. For example, in some orchestration workflows, the IAM instance profile and policy are created first in order to apply to the instance at creation time, but the of course the instance IDs for the policy are not known yet. Instead, in the snippet below, *Conditions* are used so only resources with the `f5_cloud_failover_label` tag can be updated. +Alternatively, for *Actions* that **do** allow resource level permissions, but the specific resource IDs may not be known ahead of time, you can leverage *Condition* statements that limit access to only those resources with a certain tag. For example, in some orchestration workflows, the IAM instance profile and policy are created first in order to apply to the instance at creation time, but of course the instance IDs for the policy are not known yet. Instead, in the snippet below, *Conditions* are used so only resources with the ``f5_cloud_failover_label`` tag can be updated. .. code-block:: json @@ -423,34 +463,50 @@ Tag the Network Interfaces in AWS: Define the Storage Account in AWS ````````````````````````````````` -1. Create an `S3 bucket in AWS `_ for Cloud Failover Extension cluster-wide file(s). - - .. WARNING:: To avoid a potential data breach, ensure the required S3 buckets are properly secured and do not have public access. See your cloud provider for best practices. - - .. sidebar:: :fonticon:`fa fa-info-circle fa-lg` Version Notice: + + - The property ``scopingName`` is available in Cloud Failover Extension v1.7.0 and later. + - Beginning v1.13.0, CFE supports Serverside Encryption on the S3 Bucket using Amazon S3-Managed Keys (SSE-S3) or KMS keys Stored in AWS Key Management Service (SSE-KMS) with either the default AWS managed key or a customer managed key. See `AWS Documentation `_ for more details on how to enable server-side encryption on the S3 bucket. + +1. Create an `S3 bucket in AWS `_ for Cloud Failover Extension cluster-wide file(s). - The property ``scopingName`` is available in Cloud Failover Extension v1.7.0 and later. + .. WARNING:: To avoid a potential data breach, ensure the required S3 buckets are properly secured and do not have public access. See your cloud provider for best practices. + 2. Update/modify the Cloud Failover ``scopingName`` value with name of your S3 bucket: .. code-block:: json + :emphasize-lines: 2 - "externalStorage":{ - "scopingName": "yourS3BucketforCloudFailover" - }, - + "externalStorage":{ + "scopingName": "yourS3BucketforCloudFailover", + "encryption": { + "serverSide": { + "enabled": true, + "algorithm": "AES256" + } + } + }, + You can also optionally update/modify the serverside encyption config. The example above uses S3-Managed Keys (SSE-S3). To use KMS (SSE-KMS), set the ``algorithm`` attribute to "aws::kms". Click `here `_ to see an example using KMS and the default AWS managed key. Click `here `_ to see an example using KMS and a customer managed key. Alternatively, if you are using the Discovery via Tag option, tag the S3 bucket with your custom key:values in the `externalStorage.scopingTags` section of the CFE declaration. .. code-block:: json + :emphasize-lines: 3 + + "externalStorage":{ + "scopingTags":{ + "f5_cloud_failover_label":"mydeployment" + }, + "encryption": { + "serverSide": { + "enabled": true, + "algorithm": "AES256" + } + } + }, - "externalStorage":{ - "scopingTags":{ - "f5_cloud_failover_label":"mydeployment" - } - }, a. Sign in to the AWS Management Console and open the Amazon S3 console. diff --git a/docs/userguide/aws.rst b/docs/userguide/aws.rst index e090d194..59c55512 100644 --- a/docs/userguide/aws.rst +++ b/docs/userguide/aws.rst @@ -79,13 +79,13 @@ Example AWS Declaration ----------------------- This example declaration shows a configuration used for the diagram above. See the :ref:`quickstart` section for steps on how to post this declaration. See the :ref:`example-declarations` section for more examples. -.. literalinclude:: ../../examples/declarations/aws-across-az-1.7.0.json +.. literalinclude:: ../../examples/declarations/aws-across-az-1.13.0.json :language: json :caption: Example AWS Declaration with Single Routing Table :tab-width: 4 :linenos: -:fonticon:`fa fa-download` :download:`aws.json <../../examples/declarations/aws-across-az-1.7.0.json>` +:fonticon:`fa fa-download` :download:`aws.json <../../examples/declarations/aws-across-az-1.13.0.json>` | @@ -121,6 +121,9 @@ In order to successfully implement CFE in AWS, you need an AWS Identity and Acce s3:ListAllMyBuckets \* Current Account externalStorage To discover (using scopingTags) bucket used for failover state file. s3:ListBucket S3 Bucket ID Optional externalStorage To return information about a bucket. s3:PutObject S3 Bucket ID/Key Optional externalStorage To write failover state file. + kms:DescribeKey KMS Encryption Key ID Optional externalStorage To write failover state file when using a **customer managed** KMS key for server-side encryption. + kms:GenerateDataKey KMS Encryption Key ID Optional externalStorage To write failover state file when using a **customer managed** KMS key for server-side encryption. + kms:Decrypt KMS Encryption Key ID Optional externalStorage To write failover state file when using a **customer managed** KMS key for server-side encryption. ======================================== ============================== ======================= ======================= ===================================================================================================================== | @@ -224,6 +227,26 @@ Below is an example F5 policy that includes IAM roles. ], "Resource": "arn:*:s3:::/*" }, + { + "Action": [ + "s3:PutObject" + ], + "Condition": { + "Null": { + "s3:x-amz-server-side-encryption": true + } + }, + "Effect": "Deny", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:*:s3:::/*" + ] + ] + }, + "Sid": "DenyPublishingUnencryptedResources" + }, { "Effect": "Allow", "Action": [ @@ -313,6 +336,25 @@ Below is an example F5 policy that includes IAM roles. | +NOTE: If a customer managed KMS Encryption Key is used for server-side encryption on the S3 bucket, the following permissions are required: + +.. code-block:: json + + { + "Effect": "Allow", + "Action": [ + "kms:DescribeKey", + "kms:GenerateDataKey", + "kms:Decrypt" + ], + "Resource": "arn:aws:kms:::key/" + }, + +| + +To limit encryption to a specific type or for more information, see `AWS Documentation `_. + + Alternatively, for *Actions* that **do** allow resource level permissions, but the specific resource IDs may not be known ahead of time, you can leverage *Condition* statements that limit access to only those resources with a certain tag. For example, in some orchestration workflows, the IAM instance profile and policy are created first in order to apply to the instance at creation time, but the of course the instance IDs for the policy are not known yet. Instead, in the snippet below, *Conditions* are used so only resources with the `f5_cloud_failover_label` tag can be updated. .. code-block:: json @@ -400,36 +442,53 @@ Tag the Network Interfaces in AWS: Define the Storage Account in AWS ````````````````````````````````` +.. sidebar:: :fonticon:`fa fa-info-circle fa-lg` Version Notice: -1. Create an `S3 bucket in AWS `_ for Cloud Failover Extension cluster-wide file(s). + - The property ``scopingName`` is available in Cloud Failover Extension v1.7.0 and later. + - Beginning v1.13.0, CFE supports Serverside Encryption on the S3 Bucket using Amazon S3-Managed Keys (SSE-S3) or KMS keys Stored in AWS Key Management Service (SSE-KMS) with either the default AWS managed key or a customer managed key. See `AWS Documentation `_ for more details on how to enable server-side encryption on the S3 bucket. + + +1. Create an `S3 bucket in AWS `_ for Cloud Failover Extension cluster-wide file(s). + .. WARNING:: To avoid a potential data breach, ensure the required S3 buckets are properly secured and do not have public access. See your cloud provider for best practices. -.. sidebar:: :fonticon:`fa fa-info-circle fa-lg` Version Notice: - - The property ``scopingName`` is available in Cloud Failover Extension v1.7.0 and later. -2. Update/modify the Cloud Failover ``scopingName`` value with name of your S3 bucket: +2. Update/modify the Cloud Failover ``scopingName`` value with **name** of your S3 bucket: .. code-block:: json + :emphasize-lines: 2 - "externalStorage":{ - "scopingName": "yourS3BucketforCloudFailover" - }, - - | + "externalStorage":{ + "scopingName": "yourS3BucketforCloudFailover", + "encryption": { + "serverSide": { + "enabled": true, + "algorithm": "AES256" + } + } + }, + You can also optionally update/modify the serverside encyption config. The example above uses S3-Managed Keys (SSE-S3). To use KMS (SSE-KMS), set the ``algorithm`` attribute to "aws::kms". Click `here `_ to see an example using KMS and the default AWS managed key. Click `here `_ to see an example using KMS and a customer managed key. Alternatively, if you are using the Discovery via Tag option, tag the S3 bucket with your custom key:values in the `externalStorage.scopingTags` section of the CFE declaration. .. code-block:: json + :emphasize-lines: 3 "externalStorage":{ "scopingTags":{ "f5_cloud_failover_label":"mydeployment" + }, + "encryption": { + "serverSide": { + "enabled": true, + "algorithm": "AES256" + } } }, + a. Sign in to the AWS Management Console and open the Amazon S3 console. b. In the :guilabel:`Bucket name` list, choose the name of the bucket. diff --git a/docs/userguide/example-declarations.rst b/docs/userguide/example-declarations.rst index 982fab03..89ba4bf7 100644 --- a/docs/userguide/example-declarations.rst +++ b/docs/userguide/example-declarations.rst @@ -121,9 +121,38 @@ This example shows a BIG-IP cluster managing route tables in multiple subscripti :fonticon:`fa fa-download` :download:`azureRouteTablesInMutipleSubscriptions.json <../../examples/declarations/azureRouteTablesInMutipleSubscriptions.json>` +.. _aws-sse-aws-key: -Example Declaration Setting the Log Level ------------------------------------------ +AWS KMS Server-side encryption (SSE-KMS) Using Default AWS Managed Key +---------------------------------------------------------------------- +This example shows how to configure CFE when the S3 bucket used for failover state uses server-side KMS encryption with the default AWS managed key. + +.. literalinclude:: ../../examples/declarations/aws-s3-server-side-encryption-aws-key.json + :language: json + :caption: AWS Server-side encryption with AWS managed key + :tab-width: 4 + :linenos: + :emphasize-lines: 10-15 + +:fonticon:`fa fa-download` :download:`aws-s3-server-side-encryption-aws-key.json <../../examples/declarations/aws-s3-server-side-encryption-aws-key.json>` + +.. _aws-sse-custom-key: + +AWS KMS Server-side encryption (SSE-KMS) Using Customer Managed Key +------------------------------------------------------------------- +This example shows how to configure CFE when the S3 bucket used for failover state uses server-side KMS encryption with a customer-provided key. Note: The ``keyId`` should be the actual ID, not the `arn` or `alias`. + +.. literalinclude:: ../../examples/declarations/aws-s3-server-side-encryption-custom-key.json + :language: json + :caption: AWS Server-side encryption with custom key + :tab-width: 4 + :linenos: + :emphasize-lines: 12-18 + +:fonticon:`fa fa-download` :download:`aws-s3-server-side-encryption-custom-key.json <../../examples/declarations/aws-s3-server-side-encryption-custom-key.json>` + +Setting the Log Level +--------------------- You set the log level in the controls class. To see more information about editing the controls class, see :ref:`logging-ref`. diff --git a/examples/declarations/aws-across-az-1.13.0.json b/examples/declarations/aws-across-az-1.13.0.json new file mode 100644 index 00000000..55975d87 --- /dev/null +++ b/examples/declarations/aws-across-az-1.13.0.json @@ -0,0 +1,61 @@ +{ + "class": "Cloud_Failover", + "environment": "aws", + "controls": { + "class": "Controls", + "logLevel": "silly" + }, + "externalStorage": { + "scopingName": "myCloudFailoverBucket", + "encryption": { + "serverSide": { + "enabled": true, + "algorithm": "AES256" + } + } + }, + "failoverAddresses": { + "enabled": true, + "scopingTags": { + "f5_cloud_failover_label": "mydeployment" + }, + "addressGroupDefinitions": [ + { + "type": "elasticIpAddress", + "scopingAddress": "1.1.1.1", + "vipAddresses": [ + "10.0.12.101", + "10.0.22.101" + ] + }, + { + "type": "elasticIpAddress", + "scopingAddress": "2.2.2.2", + "vipAddresses": [ + "10.0.12.102", + "10.0.22.102" + ] + } + ] + }, + "failoverRoutes": { + "enabled": true, + "routeGroupDefinitions": [ + { + "scopingName": "rtb-11111111111111111", + "scopingAddressRanges": [ + { + "range": "0.0.0.0/0" + } + ], + "defaultNextHopAddresses": { + "discoveryType": "static", + "items": [ + "10.0.13.11", + "10.0.23.11" + ] + } + } + ] + } +} diff --git a/examples/declarations/aws-s3-server-side-encryption-aws-key.json b/examples/declarations/aws-s3-server-side-encryption-aws-key.json new file mode 100644 index 00000000..c04b7705 --- /dev/null +++ b/examples/declarations/aws-s3-server-side-encryption-aws-key.json @@ -0,0 +1,53 @@ +{ + "class": "Cloud_Failover", + "environment": "aws", + "controls": { + "class": "Controls", + "logLevel": "silly" + }, + "externalStorage": { + "scopingName": "myCloudFailoverBucket", + "encryption": { + "serverSide": { + "enabled": true, + "algorithm": "aws:kms" + } + } + }, + "failoverAddresses": { + "enabled": true, + "scopingTags": { + "f5_cloud_failover_label": "mydeployment" + }, + "addressGroupDefinitions": [ + { + "type": "networkInterfaceAddress", + "scopingAddress": "10.0.12.101" + }, + { + "type": "networkInterfaceAddress", + "scopingAddress": "10.0.12.102" + } + ] + }, + "failoverRoutes": { + "enabled": true, + "routeGroupDefinitions": [ + { + "scopingName": "rtb-11111111111111111", + "scopingAddressRanges": [ + { + "range": "0.0.0.0/0" + } + ], + "defaultNextHopAddresses": { + "discoveryType": "static", + "items": [ + "10.0.13.11", + "10.0.13.12" + ] + } + } + ] + } +} \ No newline at end of file diff --git a/examples/declarations/aws-s3-server-side-encryption-custom-key.json b/examples/declarations/aws-s3-server-side-encryption-custom-key.json new file mode 100644 index 00000000..18bc85f8 --- /dev/null +++ b/examples/declarations/aws-s3-server-side-encryption-custom-key.json @@ -0,0 +1,56 @@ +{ + "class": "Cloud_Failover", + "environment": "aws", + "controls": { + "class": "Controls", + "logLevel": "silly" + }, + "externalStorage": { + "scopingTags": { + "f5_cloud_failover_label": "mydeployment" + }, + "encryption": { + "serverSide": { + "enabled": true, + "algorithm": "aws:kms", + "keyId": "11111111-1111-1111-111-11111111111" + } + } + }, + "failoverAddresses": { + "enabled": true, + "scopingTags": { + "f5_cloud_failover_label": "mydeployment" + }, + "addressGroupDefinitions": [ + { + "type": "networkInterfaceAddress", + "scopingAddress": "10.0.12.101" + }, + { + "type": "networkInterfaceAddress", + "scopingAddress": "10.0.12.102" + } + ] + }, + "failoverRoutes": { + "enabled": true, + "routeGroupDefinitions": [ + { + "scopingName": "rtb-11111111111111111", + "scopingAddressRanges": [ + { + "range": "0.0.0.0/0" + } + ], + "defaultNextHopAddresses": { + "discoveryType": "static", + "items": [ + "10.0.13.11", + "10.0.13.12" + ] + } + } + ] + } +} \ No newline at end of file diff --git a/examples/declarations/aws-same-az-1.13.0.json b/examples/declarations/aws-same-az-1.13.0.json new file mode 100644 index 00000000..327253c4 --- /dev/null +++ b/examples/declarations/aws-same-az-1.13.0.json @@ -0,0 +1,53 @@ +{ + "class": "Cloud_Failover", + "environment": "aws", + "controls": { + "class": "Controls", + "logLevel": "silly" + }, + "externalStorage": { + "scopingName": "myCloudFailoverBucket", + "encryption": { + "serverSide": { + "enabled": true, + "algorithm": "AES256" + } + } + }, + "failoverAddresses": { + "enabled": true, + "scopingTags": { + "f5_cloud_failover_label": "mydeployment" + }, + "addressGroupDefinitions": [ + { + "type": "networkInterfaceAddress", + "scopingAddress": "10.0.12.101" + }, + { + "type": "networkInterfaceAddress", + "scopingAddress": "10.0.12.102" + } + ] + }, + "failoverRoutes": { + "enabled": true, + "routeGroupDefinitions": [ + { + "scopingName": "rtb-11111111111111111", + "scopingAddressRanges": [ + { + "range": "0.0.0.0/0" + } + ], + "defaultNextHopAddresses": { + "discoveryType": "static", + "items": [ + "10.0.13.11", + "10.0.13.12" + ] + } + } + ] + } + } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a567aee7..4229e5f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "f5-cloud-failover", - "version": "1.12.0", + "version": "1.13.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -99,7 +99,7 @@ "requires": { "@azure/ms-rest-azure-env": "^2.0.0", "@azure/ms-rest-js": "^2.0.4", - "adal-node": "^0.1.28" + "adal-node": "^0.2.2" }, "dependencies": { "@azure/ms-rest-js": { @@ -108,25 +108,48 @@ "integrity": "sha512-LLi4jRe/qy5IM8U2CkoDgSZp2OH+MgDe2wePmhz8uY84Svc53EhHaamVyoU6BjjHBxvCRh1vcD1urJDccrxqIw==", "dev": true, "requires": { - "@types/node-fetch": "^2.3.7", - "@types/tunnel": "0.0.1", + "@azure/core-auth": "^1.1.4", "abort-controller": "^3.0.0", "form-data": "^2.5.0", - "node-fetch": "^2.6.0", + "node-fetch": "^2.6.7", "tough-cookie": "^3.0.1", "tslib": "^1.10.0", "tunnel": "0.0.6", - "uuid": "^3.3.2", + "uuid": "^8.3.2", "xml2js": "^0.4.19" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } } }, "adal-node": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.2.3.tgz", - "integrity": "sha512-gMKr8RuYEYvsj7jyfCv/4BfKToQThz20SP71N3AtFn3ia3yAR8Qt2T3aVQhuJzunWs2b38ZsQV0qsZPdwZr7VQ==", + "version": "0.1.28", + "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.1.28.tgz", + "integrity": "sha512-98nQ5MQSyJR0ZY/R0Mue/cv4OkebRyKz4hS40GdkZU42Bq49ldHeup7UeAo/0vROMB57CX2et6IF0U/Pe1rY3A==", "dev": true, "requires": { - "@types/node": "*" + "@types/node": "^8.0.47", + "async": ">=0.6.0", + "date-utils": "*", + "jws": "3.x.x", + "request": ">= 2.52.0", + "underscore": ">= 1.3.1", + "uuid": "^3.1.0", + "xmldom": ">= 0.1.x", + "xpath.js": "~1.1.0" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } } }, "form-data": { @@ -140,6 +163,27 @@ "mime-types": "^2.1.12" } }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dev": true, + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dev": true, + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "tough-cookie": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", @@ -160,8 +204,7 @@ "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" } } }, @@ -1153,29 +1196,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.63.tgz", "integrity": "sha512-g+nSkeHFDd2WOQChfmy9SAXLywT47WZBrGS/NC5ym5PJ8c8RC6l4pbGaUW/X0+eZJnXw6/AVNEouXWhV4iz72Q==" }, - "@types/node-fetch": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.7.tgz", - "integrity": "sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==", - "dev": true, - "requires": { - "@types/node": "*", - "form-data": "^3.0.0" - }, - "dependencies": { - "form-data": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", - "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } - } - }, "abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -2525,9 +2545,9 @@ }, "dependencies": { "async": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.1.0.tgz", + "integrity": "sha512-4vx/aaY6j/j3Lw3fbCHNWP0pPaTCew3F6F3hYyl/tHs/ndmV1q7NW9T5yuJ2XAGwdQrP+6Wu20x06U4APo/iQQ==", "optional": true }, "make-dir": { @@ -2604,9 +2624,9 @@ }, "dependencies": { "async": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", + "integrity": "sha512-5mO7DX4CbJzp9zjaFXusQQ4tzKJARjNB1Ih1pVBi8wkbmXy/xzIDgEMXxWePLzt2OdFwaxfneIlT1nCiXubrPQ==", "optional": true } } @@ -5253,14 +5273,6 @@ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" }, - "json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "requires": { - "bignumber.js": "^9.0.0" - } - }, "json-edm-parser": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/json-edm-parser/-/json-edm-parser-0.1.2.tgz", @@ -9544,6 +9556,13 @@ "eyes": "0.1.x", "isstream": "0.1.x", "stack-trace": "0.0.x" + }, + "dependencies": { + "async": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", + "integrity": "sha512-5mO7DX4CbJzp9zjaFXusQQ4tzKJARjNB1Ih1pVBi8wkbmXy/xzIDgEMXxWePLzt2OdFwaxfneIlT1nCiXubrPQ==" + } } }, "word-wrap": { diff --git a/package.json b/package.json index ee878d9a..37d56562 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "f5-cloud-failover", - "version": "1.12.0", + "version": "1.13.0", "author": "F5 Networks", "license": "Apache-2.0", "repository": { @@ -125,7 +125,8 @@ "1070207", "1070443", "1075674", - "1070440" + "1070440", + "1082433" ] } } diff --git a/specs/openapi.yaml b/specs/openapi.yaml index f4156f7b..a2473563 100644 --- a/specs/openapi.yaml +++ b/specs/openapi.yaml @@ -1,6 +1,6 @@ openapi: "3.0.0" info: - version: 1.12.0 + version: 1.13.0 title: Cloud Failover (CF) Extension description: F5 Cloud Failover (CFE) is an iControl LX Extension delivered as a TMOS-independent RPM file. Installing the CFE Extension on BIG-IP enables you to declaratively configure BIG-IP devices for automatic failover in cloud environments by POSTing a single JSON declaration to CF’s declarative REST API endpoint. license: @@ -21,9 +21,9 @@ paths: schema: $ref: "#/components/schemas/InformationResponse" example: - version: "1.12.0" + version: "1.13.0" release: "1" - schemaCurrent: "1.12.0" + schemaCurrent: "1.13.0" schemaMinimum: "0.9.1" default: description: Unexpected Error diff --git a/src/nodejs/failover.js b/src/nodejs/failover.js index e3b90eb6..85ec2298 100644 --- a/src/nodejs/failover.js +++ b/src/nodejs/failover.js @@ -282,6 +282,7 @@ class FailoverClient { const customEnvironmentSettings = util.getDataByKey(this.config, 'customEnvironment') || []; let routeGroupDefinitions = util.getDataByKey(this.config, 'failoverRoutes.routeGroupDefinitions') || []; const trustedCertBundle = util.getDataByKey(this.config, 'trustedCertBundle') || ''; + const storageEncryption = util.getDataByKey(this.config, 'externalStorage.encryption') || []; const routeTags = util.getDataByKey(this.config, 'failoverRoutes.scopingTags') || []; const routeAddressRanges = (util.getDataByKey( this.config, 'failoverRoutes.scopingAddressRanges' @@ -312,6 +313,7 @@ class FailoverClient { customEnvironment: customEnvironmentSettings, routeGroupDefinitions, trustedCertBundle, + storageEncryption, storageTags: util.getDataByKey(this.config, 'externalStorage.scopingTags'), storageName: util.getDataByKey(this.config, 'externalStorage.scopingName'), subscriptions: (util.getDataByKey( diff --git a/src/nodejs/providers/abstract/cloud.js b/src/nodejs/providers/abstract/cloud.js index 667514e0..1298530b 100644 --- a/src/nodejs/providers/abstract/cloud.js +++ b/src/nodejs/providers/abstract/cloud.js @@ -54,6 +54,7 @@ class AbstractCloud { * { 'type': 'address': 'items': [], tag: null} * @param {Object} [options.storageTags] - storage tags to filter on { 'key': 'value' } * @param {Object} [options.storageName] - storage scoping name + * @param {Object} [options.storageEncryption] - storage encryption options * @param {Object} [options.subnets] - subnets * @param {Object} [options.trustedCertBundle] - custom certificate bundle for cloud API calls */ @@ -66,6 +67,7 @@ class AbstractCloud { this.routeGroupDefinitions = options.routeGroupDefinitions || {}; this.storageTags = options.storageTags || {}; this.storageName = options.storageName || ''; + this.storageEncryption = options.storageEncryption || null; this.subnets = options.subnets || {}; this.trustedCertBundle = options.trustedCertBundle || ''; } diff --git a/src/nodejs/providers/aws/cloud.js b/src/nodejs/providers/aws/cloud.js index 344ec7e7..4e320efb 100644 --- a/src/nodejs/providers/aws/cloud.js +++ b/src/nodejs/providers/aws/cloud.js @@ -132,6 +132,14 @@ class Cloud extends AbstractCloud { Bucket: this.s3BucketName, Key: s3Key }; + if (this.storageEncryption + && this.storageEncryption.serverSide + && this.storageEncryption.serverSide.enabled) { + params.ServerSideEncryption = this.storageEncryption.serverSide.algorithm; + if (this.storageEncryption.serverSide.keyId) { + params.SSEKMSKeyId = this.storageEncryption.serverSide.keyId; + } + } this.s3.putObject(params).promise() .then(() => resolve()) .catch((err) => reject(err)); diff --git a/src/nodejs/providers/azure/cloud.js b/src/nodejs/providers/azure/cloud.js index 1aaff165..88a30a7a 100644 --- a/src/nodejs/providers/azure/cloud.js +++ b/src/nodejs/providers/azure/cloud.js @@ -503,7 +503,7 @@ class Cloud extends AbstractCloud { let matchedTags = 0; tagKeys.forEach((tagKey) => { if (sa.tags && Object.keys(sa.tags).indexOf(tagKey) !== -1 - && sa.tags[tagKey] === tags[tagKey]) { + && sa.tags[tagKey] === tags[tagKey]) { matchedTags += 1; } }); @@ -804,6 +804,8 @@ class Cloud extends AbstractCloud { const theirNicIpConfigs = this._getIpConfigs(theirNic.ipConfigurations); const myNicIpConfigs = this._getIpConfigs(myNic.ipConfigurations); + this.logger.silly('checking for NIC operations for my/their NIC pair:', myNic.name, theirNic.name); + for (let i = theirNicIpConfigs.length - 1; i >= 0; i -= 1) { for (let t = failoverAddresses.length - 1; t >= 0; t -= 1) { if (failoverAddresses[t] === theirNicIpConfigs[i].privateIPAddress) { @@ -1119,6 +1121,7 @@ class Cloud extends AbstractCloud { * @returns {Promise} - A Promise that is resolved network interface operations */ _generateNetworkInterfaceOperations(addresses, addressGroupDefinitions) { + const failoverAddresses = []; const operations = { disassociate: [], associate: [] @@ -1129,27 +1132,37 @@ class Cloud extends AbstractCloud { .then((results) => { const nics = results[0]; addressGroupDefinitions.forEach((item) => { - const failoverAddresses = [item.scopingAddress]; - const parsedNics = this._parseNics(nics, addresses.localAddresses, failoverAddresses); - if (parsedNics.mine.length === 0 || parsedNics.theirs.length === 0) { - this.logger.warning('Problem with discovering network interfaces parsedNics'); - return Promise.resolve({ - publicAddresses: {}, - interfaces: operations, - loadBalancerAddresses: {} - }); - } - const nicOperations = this._checkForNicOperations( - parsedNics.mine[0].nic, - parsedNics.theirs[0].nic, - failoverAddresses - ); - if (nicOperations.disassociate && nicOperations.associate) { - operations.disassociate.push(nicOperations.disassociate); - operations.associate.push(nicOperations.associate); - } - return item; + failoverAddresses.push(item.scopingAddress); }); + const parsedNics = this._parseNics(nics, addresses.localAddresses, failoverAddresses); + if (parsedNics.mine.length === 0 || parsedNics.theirs.length === 0) { + this.logger.warning('Problem with discovering network interfaces parsedNics'); + return Promise.resolve({ + publicAddresses: {}, + interfaces: operations, + loadBalancerAddresses: {} + }); + } + for (let s = parsedNics.mine.length - 1; s >= 0; s -= 1) { + for (let h = parsedNics.theirs.length - 1; h >= 0; h -= 1) { + const theirNic = parsedNics.theirs[h].nic; + const myNic = parsedNics.mine[s].nic; + theirNic.tags = theirNic.tags ? theirNic.tags : this._normalizeTags(theirNic.TagSet); + myNic.tags = myNic.tags ? myNic.tags : this._normalizeTags(myNic.TagSet); + /* eslint-disable max-len */ + if (theirNic.tags[constants.NIC_TAG] === undefined || myNic.tags[constants.NIC_TAG] === undefined) { + this.logger.warning(`${constants.NIC_TAG} tag values do not match or doesn't exist for a interface`); + } else if (theirNic.tags[constants.NIC_TAG] && myNic.tags[constants.NIC_TAG] + && theirNic.tags[constants.NIC_TAG] === myNic.tags[constants.NIC_TAG]) { + const nicOperations = this._checkForNicOperations(myNic, theirNic, failoverAddresses); + + if (nicOperations.disassociate && nicOperations.associate) { + operations.disassociate.push(nicOperations.disassociate); + operations.associate.push(nicOperations.associate); + } + } + } + } this.resultAction.interfaces = operations; return Promise.resolve(); }) diff --git a/src/nodejs/schema/base_schema.json b/src/nodejs/schema/base_schema.json index 2cbb1026..76eb8928 100644 --- a/src/nodejs/schema/base_schema.json +++ b/src/nodejs/schema/base_schema.json @@ -117,6 +117,7 @@ "type": "string", "$comment": "IMPORTANT: In enum array, please put current schema version first, oldest-supported version last. Keep enum array sorted most-recent-first.", "enum": [ + "1.13.0", "1.12.0", "1.11.0", "1.10.0", @@ -134,7 +135,7 @@ "1.0.0", "0.9.1" ], - "default": "1.12.0" + "default": "1.13.0" }, "$schema": { "title": "Schema", @@ -280,6 +281,39 @@ "title": "External Storage", "description": "External storage this deployment will manage.", "type": "object", + "properties": { + "encryption": { + "description": "Settings related to encrypted storage.", + "type": "object", + "properties": { + "serverSide": { + "description": "Settings related to server-side encryption.", + "type": "object", + "properties": { + "enabled": { + "description": "For enabling server-side encryption.", + "type": "boolean", + "default": false + }, + "algorithm": { + "description": "Encryption algorithm used for server-side encryption.", + "type": "string", + "enum": [ + "AES256", + "aws:kms" + ], + "default": "aws:kms" + }, + "keyId": { + "description": "Client-managed key ID used for server-side encryption.", + "type": "string", + "examples": ["myKeyId"] + } + } + } + } + } + }, "oneOf": [ { "properties": { diff --git a/test/functional/tests/providers/azure/tests.js b/test/functional/tests/providers/azure/tests.js index b09ee0ce..da5a57b8 100644 --- a/test/functional/tests/providers/azure/tests.js +++ b/test/functional/tests/providers/azure/tests.js @@ -45,7 +45,7 @@ function networkInterfaceMatch(networkInterfaces, selfIps, virtualAddresses) { networkInterfaces.forEach((nic) => { nic.ipConfigurations.forEach((ipConfiguration) => { selfIps.forEach((address) => { - if (ipConfiguration.privateIPAddress === address) { + if (nic.provisioningState === 'Succeeded' && ipConfiguration.primary === true && ipConfiguration.privateIPAddress === address) { myNics.push(nic); } }); @@ -55,7 +55,7 @@ function networkInterfaceMatch(networkInterfaces, selfIps, virtualAddresses) { myNics.forEach((nic) => { nic.ipConfigurations.forEach((ipConfiguration) => { virtualAddresses.forEach((address) => { - if (ipConfiguration.privateIPAddress === address && nic.provisioningState === 'Succeeded') { + if (nic.provisioningState === 'Succeeded' && ipConfiguration.primary === false && ipConfiguration.privateIPAddress === address) { match = true; } }); diff --git a/test/unit/providers/awsProviderTests.js b/test/unit/providers/awsProviderTests.js index f24a847c..4bfe8f92 100644 --- a/test/unit/providers/awsProviderTests.js +++ b/test/unit/providers/awsProviderTests.js @@ -549,6 +549,7 @@ describe('Provider - AWS', () => { assert.fail(); })); }); + describe('function _getPrivateSecondaryIPs', () => { const describeNetworkInterfacesResponse = { NetworkInterfaces: [ @@ -654,6 +655,7 @@ describe('Provider - AWS', () => { }); }); }); + describe('function _generatePublicAddressOperations', () => { const EIPdata = [ { @@ -691,6 +693,7 @@ describe('Provider - AWS', () => { assert.fail(); })); }); + describe('function _associatePublicAddress', () => { const allocationId = 'eipalloc-0b5671ebba3628edd'; const networkInterfaceId = 'eni-0157ac0f9506af78b'; @@ -744,6 +747,7 @@ describe('Provider - AWS', () => { }); }); }); + describe('function _disassociatePublicAddress', () => { let passedParams; const associationIdToDisassociate = 'eipassoc-00523b2b8b8c01793'; @@ -770,6 +774,7 @@ describe('Provider - AWS', () => { assert.fail(); })); }); + describe('function _reassociatePublicAddresses', () => { it('should call _disassociatePublicAddress with correct params', () => { const passedParams = []; @@ -1543,8 +1548,15 @@ describe('Provider - AWS', () => { describe('function uploadDataToStorage', () => { let passedParams; + const thisMockInitData = { + storageEncryption: { + serverSide: { + enabled: false + } + } + }; - it('should pass correct params to putObject', () => provider.init(mockInitData) + it('should pass correct params to putObject', () => provider.init(thisMockInitData) .then(() => { provider.s3BucketName = 'myfailoverbucket'; @@ -1567,6 +1579,90 @@ describe('Provider - AWS', () => { })); }); + describe('function uploadDataToStorage encrypted with AWS managed key', () => { + let passedParams; + const _s3FileParamsStubEncrypted = { + Body: 's3 state file body', + Bucket: 'myfailoverbucket', + Key: 'f5cloudfailover/file.json', + ServerSideEncryption: 'aws:kms' + }; + + const thisMockInitData = { + storageEncryption: { + serverSide: { + enabled: true, + algorithm: 'aws:kms' + } + } + }; + + it('should pass correct params to putObject with AWS managed key', () => provider.init(thisMockInitData) + .then(() => { + provider.s3BucketName = 'myfailoverbucket'; + + provider.s3.putObject = sinon.stub() + .callsFake((params) => { + passedParams = params; + return { + promise() { + return Promise.resolve(); + } + }; + }); + return provider.uploadDataToStorage('file.json', _s3FileParamsStubEncrypted.Body); + }) + .then(() => { + assert.deepEqual(passedParams, _s3FileParamsStubEncrypted); + }) + .catch(() => { + assert.fail(); + })); + }); + + describe('function uploadDataToStorage encrypted with customer key', () => { + let passedParams; + const _s3FileParamsStubEncryptedCustomerKey = { + Body: 's3 state file body', + Bucket: 'myfailoverbucket', + Key: 'f5cloudfailover/file.json', + ServerSideEncryption: 'aws:kms', + SSEKMSKeyId: 'mrk-e6113680390641cab86a87e821e43764' + }; + + const thisMockInitData = { + storageEncryption: { + serverSide: { + enabled: true, + algorithm: 'aws:kms', + keyId: 'mrk-e6113680390641cab86a87e821e43764' + } + } + }; + + it('should pass correct params to putObject with customer key', () => provider.init(thisMockInitData) + .then(() => { + provider.s3BucketName = 'myfailoverbucket'; + + provider.s3.putObject = sinon.stub() + .callsFake((params) => { + passedParams = params; + return { + promise() { + return Promise.resolve(); + } + }; + }); + return provider.uploadDataToStorage('file.json', _s3FileParamsStubEncryptedCustomerKey.Body); + }) + .then(() => { + assert.deepEqual(passedParams, _s3FileParamsStubEncryptedCustomerKey); + }) + .catch(() => { + assert.fail(); + })); + }); + describe('function downloadDataFromStorage', () => { let passedParams; const mockResponseBody = { foo: 'bar' }; diff --git a/test/unit/providers/azureProviderTests.js b/test/unit/providers/azureProviderTests.js index b4dc9e68..5ffa49e1 100644 --- a/test/unit/providers/azureProviderTests.js +++ b/test/unit/providers/azureProviderTests.js @@ -369,6 +369,7 @@ describe('Provider - Azure', () => { }) .catch((err) => Promise.reject(err)); }); + it('should validate updateAddresses does not perform discovery due to mismatched nic tags', () => { const localAddresses = ['2.2.2.2']; const failoverAddresses = ['10.10.10.10']; @@ -422,6 +423,7 @@ describe('Provider - Azure', () => { }) .catch((err) => Promise.reject(err)); }); + it('validate _updateNic promise callback for valid case', () => { provider.primarySubscriptionId = mockSubscriptionId; provider.networkClients[mockSubscriptionId] = sinon.stub(); @@ -1383,7 +1385,7 @@ describe('Provider - Azure', () => { failoverAddresses: [] }; const addresses2 = { - localAddresses: ['10.10.10.4'], + localAddresses: ['10.10.10.4', '10.10.11.4'], failoverAddresses: [] }; // Use nic01 and nic02 to validate across-net, @@ -1433,7 +1435,7 @@ describe('Provider - Azure', () => { } ] }; - // Use nic03 and nic04 to validate same-net, moving the secondary ipConfigurations + // Use nic03-nic06 to validate same-net, moving the secondary ipConfigurations const nic03 = { id: 'test-nic03', name: 'nic03', @@ -1464,7 +1466,11 @@ describe('Provider - Azure', () => { }, provisioningState: 'Succeeded' } - ] + ], + tags: { + f5_cloud_failover_label: 'tagsNic', + f5_cloud_failover_nic_map: 'external' + } }; const nic04 = { id: 'test-nic04', @@ -1480,7 +1486,55 @@ describe('Provider - Azure', () => { }, provisioningState: 'Succeeded' } - ] + ], + tags: { + f5_cloud_failover_label: 'tagsNic', + f5_cloud_failover_nic_map: 'external' + } + }; + const nic05 = { + id: 'test-nic05', + name: 'nic05', + provisioningState: 'Succeeded', + type: 'networkInterfaces', + ipConfigurations: [ + { + privateIPAddress: '10.10.11.3', + primary: true, + provisioningState: 'Succeeded' + }, + { + privateIPAddress: '10.10.11.20', + primary: false, + provisioningState: 'Succeeded' + }, + { + privateIPAddress: '10.10.11.21', + primary: false, + provisioningState: 'Succeeded' + } + ], + tags: { + f5_cloud_failover_label: 'tagsNic', + f5_cloud_failover_nic_map: 'internal' + } + }; + const nic06 = { + id: 'test-nic06', + name: 'nic06', + provisioningState: 'Succeeded', + type: 'networkInterfaces', + ipConfigurations: [ + { + privateIPAddress: '10.10.11.4', + primary: true, + provisioningState: 'Succeeded' + } + ], + tags: { + f5_cloud_failover_label: 'tagsNic', + f5_cloud_failover_nic_map: 'internal' + } }; const publicIpResponse = { id: 'vip-pip1', @@ -1602,28 +1656,53 @@ describe('Provider - Azure', () => { 'nic03', 'nic04' ] + }, + { + type: 'networkInterfaceAddress', + scopingAddress: '10.10.11.20', + networkInterfaces: [ + 'nic05', + 'nic06' + ] + }, + { + type: 'networkInterfaceAddress', + scopingAddress: '10.10.11.21', + networkInterfaces: [ + 'nic05', + 'nic06' + ] } ]; provider.primarySubscriptionId = mockSubscriptionId; provider.networkClients[mockSubscriptionId] = sinon.stub(); provider.networkClients[mockSubscriptionId].networkInterfaces = sinon.stub(); provider.networkClients[mockSubscriptionId].networkInterfaces.list = sinon.stub((error, callback) => { - callback(error, [nic03, nic04]); + callback(error, [nic03, nic04, nic05, nic06]); }); return provider.discoverAddressOperationsUsingDefinitions(addresses2, networkGroupDefinitions, options) .then((response) => { const disasociate = response.interfaces.disassociate; const associate = response.interfaces.associate; - assert.strictEqual(disasociate[0][1], 'nic03'); - assert.strictEqual(disasociate[0][2].name, 'nic03'); - assert.strictEqual(disasociate[0][2].ipConfigurations[0].privateIPAddress, '10.10.10.3'); - assert.strictEqual(associate[0][1], 'nic04'); - assert.strictEqual(associate[0][2].name, 'nic04'); - assert.strictEqual(associate[0][2].ipConfigurations[0].privateIPAddress, '10.10.10.4'); - assert.strictEqual(associate[0][2].ipConfigurations[1].privateIPAddress, '10.10.10.20'); - assert.strictEqual(associate[0][2].ipConfigurations[2].privateIPAddress, '10.10.10.21'); - assert.strictEqual(associate[0][2].ipConfigurations[1].publicIPAddress.id, 'vip-pip6'); + assert.strictEqual(disasociate[1][1], 'nic03'); + assert.strictEqual(disasociate[1][2].name, 'nic03'); + assert.strictEqual(disasociate[1][2].ipConfigurations[0].privateIPAddress, '10.10.10.3'); + assert.strictEqual(associate[1][1], 'nic04'); + assert.strictEqual(associate[1][2].name, 'nic04'); + assert.strictEqual(associate[1][2].ipConfigurations[0].privateIPAddress, '10.10.10.4'); + assert.strictEqual(associate[1][2].ipConfigurations[1].privateIPAddress, '10.10.10.21'); + assert.strictEqual(associate[1][2].ipConfigurations[2].privateIPAddress, '10.10.10.20'); + assert.strictEqual(associate[1][2].ipConfigurations[1].publicIPAddress.id, 'vip-pip7'); + assert.strictEqual(associate[1][2].ipConfigurations[2].publicIPAddress.id, 'vip-pip6'); + assert.strictEqual(disasociate[0][1], 'nic05'); + assert.strictEqual(disasociate[0][2].name, 'nic05'); + assert.strictEqual(disasociate[0][2].ipConfigurations[0].privateIPAddress, '10.10.11.3'); + assert.strictEqual(associate[0][1], 'nic06'); + assert.strictEqual(associate[0][2].name, 'nic06'); + assert.strictEqual(associate[0][2].ipConfigurations[0].privateIPAddress, '10.10.11.4'); + assert.strictEqual(associate[0][2].ipConfigurations[1].privateIPAddress, '10.10.11.21'); + assert.strictEqual(associate[0][2].ipConfigurations[2].privateIPAddress, '10.10.11.20'); }) .catch((err) => Promise.reject(err)); });