├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASE.md ├── bin ├── connect_copy ├── connect_diff └── connect_save └── examples └── codebuild └── buildspec.yml /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *development* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon-Connect-Copy User Guide 2 | 3 | The Amazon-Connect-Copy script (v1.3.5) copies components from the source Amazon Connect instance 4 | to the target instance safely, fixing all internal references. 5 | 6 | You may use Amazon-Connect-Copy to deploy an Amazon Connect instance across environments 7 | (AWS accounts or regions), or to save a backup copy of the instance for restoration 8 | when required, reducing an hours-long error-prone manual job to a few minutes of 9 | automated and reliable processing. 10 | 11 | Ids and Arns of components copied from the source instance will be re-mapped to 12 | their corresponding components in the new instance, including: 13 | 14 | - Instance (pre-existing) 15 | - Lambda functions (pre-deployed) 16 | - Lex bots (Classic) (pre-deployed) 17 | - Prompts (pre-uploaded) 18 | - Hours of operations 19 | - Queues (STANDARD type only) 20 | - Routing profiles 21 | - Contact flow modules 22 | - Contact flows 23 | 24 | The following components are not copied by Amazon-Connect-Copy (to avoid any impact on 25 | other contact centres that may happen to be using the same target instance): 26 | 27 | - Users (agents) related settings, statuses and the hierarchy 28 | - Security profiles 29 | - Phone numbers 30 | - Inbound Contact flow/IVR mappings 31 | - Outbound caller ID number for queues 32 | - Quick connects 33 | - Agent queues 34 | - Settings for existing standard queues 35 | - Note: Settings for new standard queues will still be copied 36 | - Historical metrics and reports 37 | - Contact Trace Records (CTRs) 38 | - Custom vocabularies 39 | - Rules for Contact lens and Third-party integration 40 | 41 | Amazon-Connect-Copy was designed for deployment across environments 42 | (e.g., from non-prod to prod). 43 | Considering the target instance may accommodate multiple contact centres, 44 | Amazon-Connect-Copy does not remove any target instance components which are 45 | not found in the source instance. If there are multiple contact centres sharing 46 | the same Amazon Connect instance, it is a good practice to prefix contact centre 47 | specific components with their individual Contact Centre Codes (CCCs). 48 | 49 | A note to developers: To contribute, please read [CONTRIBUTING.md](./CONTRIBUTING.md) first. We will accept PRs on the *development* branch only. Thank you. 50 | 51 | ## Installation 52 | 53 | - Install [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html). 54 | - Recommend installing the latest version of AWS CLI (2.9.4 or higher). 55 | - Install [jq](https://stedolan.github.io/jq/) (if not already installed on your platform). 56 | - Require `jq` version 1.6 or higher. 57 | - Copy `bin/*` to your Shell search path (e.g., `cp bin/* /usr/local/bin/`). 58 | 59 | ## Example 60 | 61 | **Please replace names in the example with names specific to your use case.** 62 | 63 | ``` 64 | connect_save -p source-profile -c CCC source-connect-alias 65 | connect_save -p target-profile -c CCC target-connect-alias 66 | connect_diff source-connect-alias target-connect-alias helper 67 | # Dry run 68 | connect_copy -d helper 69 | # Real run 70 | connect_copy helper 71 | ``` 72 | 73 | In this example: 74 | - We copy from instance `source-connect-alias` to instance `target-connect-alias`. 75 | - Credentials for the source and target instances are in AWS profiles 76 | `source-profile` and `target-profile` respectively. 77 | - You may use the same profile for `source-profile` and `target-profile`, 78 | as long as that profile allows access to both the source and the target instances 79 | (typically when they are in the same AWS account). 80 | - Only contact flows and modules with names prefixed by the `CCC` 81 | Contact Centre Code will be copied to the target instance. 82 | - Differences of the two instances (including profile specifications) 83 | will be saved in directory `helper`. 84 | 85 | ## Copying process 86 | 87 | Note: All names in Amazon Connect are case sensitive. 88 | 89 | ### Pre-steps 90 | 91 | - Make sure no one else is making changes to either the source or the 92 | target instances, or any Lambda functions or Lex bots (Classic) they integrate with. 93 | - Deploy all Lambda functions required by the target instance. 94 | - Build all Lex bots (Classic) required by the target instance. 95 | - Upload all required prompts to the target instance. 96 | - The prompt names need to be *exactly* the same as their counterparts 97 | in the source instance. 98 | - For an incremental instance update with contact flow or module **name changes**, 99 | before the copying please manually change the corresponding flow or module names in 100 | the *target* instance. Otherwise, contact flows and modules with new names will 101 | be created in the target instance, and those with old names will be left untouched. 102 | 103 | ### Copying 104 | 105 | - Set up [named profiles](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html) for AWS CLI access to Amazon Connect. 106 | - `` for the source instance 107 | - `` for the target instance 108 | - This step is optional if your default profile already has access to both 109 | the source and the target instances. If not sure, skip this step for now. 110 | You only need to set up the profiles if `connect_save` fail due to a permission error. 111 | - `cd` to an empty working directory (e.g., `md ; cd `). 112 | - Optionally, run `connect_save` with no arguments to show the help message: 113 | ``` 114 | Usage: connect_save [-?fsev] [-p aws_profile] [-c contact_flow_prefix] [-G ignore_prefix] instance_alias 115 | Retrieve components from an Amazon Connect instance into plain files 116 | 117 | instance_alias Alias of the Connect instance (or path to the directory to save, with the alias being the basename) 118 | -f Force removal of existing instance_alias directory 119 | -s Skip unpublished contact flow modules and contact flows with an error (instead of failing) 120 | -e Proceed even when the system may not encode Extended ASCII characters properly 121 | -v Show version of this script 122 | -p profile AWS Profile to use 123 | -c contact_flow_prefix Prefix of Contact Flows and Modules to be copied (all others will be ignored) - Default is to copy all 124 | -G ignore_prefix Ignore hours, queues, routing profiles, flows or modules with names prefixed with ignore_prefix 125 | -C codepage Override the auto-detected codepage (e.g., use '-C CP1252' for Gitbash ANSI if experiencing encoding issues) 126 | -? Help 127 | ``` 128 | - `` can be a directory path. 129 | - `.log` will be produced by `connect_save`. 130 | - Run `connect_save -p -c ` . 131 | - Run `connect_save -p -c ` . 132 | - Optionally, run `connect_diff` with no arguments to show the help message: 133 | ``` 134 | Usage: connect_diff [-?fev] [-l lambda_prefix_a=lambda_prefix_b] [-b lex_bot_prefix_a=lex_bot_prefix_b] instance_alias_a instance_alias_b helper 135 | Based on connect_save result on Amazon Connect instance A and B, 136 | find the differences and produce helper files to safely copy components from A to B. 137 | 138 | instance_alias_a Alias of the Connect instance A 139 | instance_alias_b Alias of the Connect instance B 140 | (Aliases can be a path to the directory where the instance was saved using connect_save.) 141 | helper Name of the helper directory 142 | -f Force removal of existing helper directory 143 | -e Proceed even when the system may not encode Extended ASCII characters properly 144 | -v Show version of this script 145 | -l lambda_prefix_a=lambda_prefix_b 146 | Lambda function name prefixes for instances A and B (if different) to be replaced during copying 147 | -b lex_bot_prefix_a=lex_bot_prefix_b 148 | Lex bot (Classic) name prefixes for instances A and B (if different) to be replaced during copying 149 | -? Help 150 | 151 | Note: This script create files in the helper directory without changing any instance component files. 152 | ``` 153 | - Run `connect_diff -l = -b = ` . 154 | - Optionally, check under the helper directory `` to find the four helper files: 155 | - `helper.new` - components to create 156 | (those found in the source but not in the target); You may remove components that you do not want to be created from `helper.new`. 157 | - `helper.old` - components to update 158 | (those found in the source and also in the target); You may remove components that you do not want to be updated from `helper.old`. 159 | - `helper.sed` - SED script to fix references 160 | (so target components will not refer to any components in the source) 161 | - `helper.var` - variables of the two instances 162 | (instance A is the source, and instance B is the target) 163 | - Optionally, run `connect_copy` with no arguments to show the help message: 164 | ``` 165 | Usage: connect_copy [-?dev] helper 166 | Copy Amazon Connect instance A to instance B safely, based on the 167 | connect_save and connect_diff results, under the helper directory 168 | creating new components in helper.new, updating old components in helper.old, 169 | and updating references defined in helper.sed. 170 | 171 | helper Name of the helper directory 172 | -d Dry run - Run through the script but not updating the target instance 173 | -e Proceed even when the system may not encode Extended ASCII characters properly 174 | -v Show version of this script 175 | -? Help 176 | ``` 177 | - Optionally, verify the helper by dry-running `connect_copy -d ` . 178 | - Check if the proposed changes are as what you would expect from the output. 179 | - AWS CLI commands to execute can be found in `.log` for your reference. 180 | - Please do not run the log file as an executable. Run `connect_copy` 181 | without `-d` (dry-run) to perform the actual copying. 182 | - Run `connect_copy ` . 183 | - Verify if the target instance contains all source instance components 184 | of the latest version, with all internal references, Lambda invocations and Lex bot (Classic) input 185 | properly adjusted. 186 | 187 | ### Post-steps 188 | 189 | - Login to Amazon Connect target instance. 190 | - Open Phone numbers. 191 | - Check Contact flow/IVR of all phone numbers. 192 | - If required, re-map phone numbers to the new Inbound Contact flows/IVR. 193 | - Open Queues. 194 | - For new outbound queues created, set these if required: 195 | - Outbound caller ID name 196 | - Outbound caller ID number 197 | - Outbound whisper flow 198 | 199 | ## Backup an Amazon Connect instance using Amazon-Connect-Copy 200 | 201 | You may restore an Amazon Connect instance from a previous backup copy saved by `connect_save`. 202 | 203 | Example: 204 | - Save a backup copy of the Amazon Connect instance. 205 | ``` 206 | connect_save -p / 207 | ``` 208 | - Restore the same instance from the backup copy. 209 | - Save the current copy (the one to be restored). 210 | ``` 211 | connect_save -p / 212 | ``` 213 | - Diff the current copy with the backup copy. 214 | ``` 215 | connect_diff / / 216 | ``` 217 | - Optionally, dry run to verify restoration changes. 218 | ``` 219 | connect_copy -d 220 | ``` 221 | - Restore the instance. 222 | ``` 223 | connect_copy 224 | ``` 225 | 226 | ## Useful Tips 227 | 228 | - DO NOT reuse the *target* instance directory and the *helper* directory. 229 | Remove these two directories after copying. 230 | - If you want to keep a backup of the target instance after copying, 231 | run `connect_save` again on the target instance. 232 | - This script has been tested with AWS CLI 2.9.4, which supports the latest 233 | Amazon Connect features, including Contact Flow Modules. 234 | (Even your instances may not be using all latest Amazon Connect features, 235 | the script will check them and therefore require the latest AWS CLI.) 236 | - Make sure both the source instance and the target instance remain unaltered 237 | by anyone during the entire copying process (save, diff and copy). 238 | - `connect_diff` only creates the helper directory and will not change anything 239 | in the source and the target instance directories. 240 | - `connect_copy` will change files in the helper directory, and when in 241 | non-dry-run mode, will change the target instance directory as well. 242 | i.e., `connect_copy` is not idempotent to the target and helper directory. 243 | - `connect_diff` and `connect_copy` do not change the source instance directory, 244 | so the source can serve as a backup or be used to copy to multiple target instances. 245 | - If relative paths are specified in instance aliases, make sure you are running 246 | `connect_diff` and `connect_copy` from the same directory, so that `connect_copy` 247 | will resolve the relative paths correctly. 248 | - The Amazon-Connect-Copy script does not affect any instance-specific settings outside of 249 | the Amazon Connect console, such as 250 | [Amazon Connect service quotas](https://docs.aws.amazon.com/connect/latest/adminguide/amazon-connect-service-limits.html). 251 | - For Hours of operations, contact flows, and contact flow modules, Description is required in the target instance. If the Description is missing in the source instance, the component name will be used as Description in the target instance in the copying process. (For other component types, Description is optional.) 252 | - The Amazon-Connect-Copy script requires the following actions to be allowed. Please configure your AWS Profile with a role authorised to perform these actions: 253 | ``` 254 | connect:AssociateBot 255 | connect:AssociateLambdaFunction 256 | connect:AssociateLexBot 257 | connect:AssociateQueueQuickConnects 258 | connect:AssociateRoutingProfileQueues 259 | connect:CreateContactFlow 260 | connect:CreateContactFlowModule 261 | connect:CreateHoursOfOperation 262 | connect:CreateQueue 263 | connect:CreateQuickConnect 264 | connect:CreateRoutingProfile 265 | connect:DeleteContactFlow 266 | connect:DeleteContactFlowModule 267 | connect:DeleteHoursOfOperation 268 | connect:DeleteQuickConnect 269 | connect:DescribeContactFlow 270 | connect:DescribeContactFlowModule 271 | connect:DescribeHoursOfOperation 272 | connect:DescribeQueue 273 | connect:DescribeQuickConnect 274 | connect:DescribeRoutingProfile 275 | connect:DisassociateRoutingProfileQueues 276 | connect:ListContactFlowModules 277 | connect:ListContactFlows 278 | connect:ListHoursOfOperations 279 | connect:ListInstances 280 | connect:ListPhoneNumbers 281 | connect:ListPrompts 282 | connect:ListQueueQuickConnects 283 | connect:ListQueues 284 | connect:ListQuickConnects 285 | connect:ListRoutingProfileQueues 286 | connect:ListRoutingProfiles 287 | connect:UpdateContactFlowContent 288 | connect:UpdateContactFlowModuleContent 289 | connect:UpdateHoursOfOperation 290 | connect:UpdateQueueHoursOfOperation 291 | connect:UpdateQueueOutboundCallerConfig 292 | connect:UpdateQuickConnectConfig 293 | connect:UpdateRoutingProfileConcurrency 294 | connect:UpdateRoutingProfileDefaultOutboundQueue 295 | ``` 296 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | - Version 1.3.5 4 | - Handle null component descriptions by setting the corresponding target description to the component name 5 | 6 | - Version 1.3.4 7 | - Copy contact flow/module description 8 | - Amend README.md 9 | 10 | - Version 1.3.3 11 | - Add `check_contact_flow()` to find broken references (missing components) in contact flows 12 | - Fix QueueConfigs limit issue when creating new routing profiles 13 | 14 | - Version 1.3.2 15 | - Fix AWS CLI max 10 queue-configs in `aws connect associate-routing-profile-queues` 16 | - Fix QueueName select in RoutingProfileQueueConfigSummaryList 17 | - Remove routing_profile_to_ignore from connect_diff 18 | - Explicit with `.` in `jq -s '.'` (instead of just `jq -s`) in connect_copy 19 | 20 | - Version 1.3.1 21 | - Fix unpublished contact flow error 22 | - Limit queue copying to STANDARD type only 23 | - Update doc 24 | 25 | - Version 1.3 26 | - Fix an error when a routing profile has no queues 27 | - Fix reference cross-reference between contact flows and modules 28 | 29 | - Version 1.2.2 30 | - Delete NumberOfAssociatedQueues and NumberOfAssociatedUsers from AWS CLI describe-routing-profile output 31 | - Error handle iconv in connect_save 32 | - Add IAM role permission to README.md 33 | -------------------------------------------------------------------------------- /bin/connect_copy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ############################################################ 4 | # 5 | # Copy Amazon Connect instance A to instance B safely 6 | # 7 | 8 | VERSION=1.3.5 9 | SCRIPT_VERSION="$(basename $0) $VERSION" 10 | 11 | AWS_CLI_MAX_QUEUE_CONFIGS=10 12 | 13 | USAGE=$(cat <&2; exit 2; } 28 | version() { echo -e "$SCRIPT_VERSION"; exit; } 29 | dos2unix() { tr -d '\r'; } 30 | 31 | error() { 32 | if [[ ! "$1" =~ ^[[:digit:]]+$ ]]; then 33 | # Not an AWS CLI error 34 | cat <&2 35 | Error: $* 36 | EOD 37 | else 38 | # An AWS CLI error 39 | line_no=$1 40 | shift 41 | cat <&2 42 | $* 43 | Error at line ${line_no}. Recommended actions: 44 | Make sure all required prompts exist in the target instance, and 45 | Install the latest AWS CLI: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html . 46 | EOD 47 | fi 48 | exit 1 49 | } 50 | 51 | hex_cmd= 52 | if [ -x "$(command -v xxd)" ]; then 53 | hex_cmd="xxd -u -p -c1" 54 | elif [ -x "$(command -v hexdump)" ]; then 55 | hex_cmd="hexdump -v -e '/1 \"%02X\n\"'" 56 | elif [ -x "$(command -v od)" ]; then 57 | hex_cmd="od -An -vtx1 | tr [:lower:] [:upper:] | for i in \$(cat); do echo \$i; done" 58 | fi 59 | test -z "$hex_cmd" && error "Cannot find any hex conversion commands. Please install one of these: xxd, hexdump, od" 60 | hex_code() { printf '%s' "$1" | eval "$hex_cmd" | while read x; do printf "%%%s" "$x"; done } 61 | 62 | path_encode() { 63 | old_lc_collate=$LC_COLLATE 64 | LC_COLLATE=C 65 | local length="${#1}" 66 | for (( i = 0; i < length; i++ )); do 67 | local c="${1:$i:1}" 68 | case $c in 69 | [a-zA-Z0-9.~_-]) printf '%s' "$c";; 70 | *) x=$(hex_code "$c"); echo -n ${x//%0D/};; 71 | esac 72 | done 73 | LC_COLLATE=$old_lc_collate 74 | } 75 | 76 | path_decode() { 77 | local path_encoded="${1//+/ }" 78 | printf '%b' "${path_encoded//%/\\x}" 79 | } 80 | 81 | diff_files() { 82 | # Compare TEMPA (sedded into TEMP1 and sorted into TEMP2) and TEMPB (sorted inline). 83 | # If same, return 0. If different, show the difference unsorted, 84 | # then ask if want to update. Return 1 if needs update and 0 otherwise. 85 | sed -f "$helper_sed" $TEMPA | tee $TEMP1 | sort > $TEMP2 86 | sort $TEMPB | diff -qbB - $TEMP2 &> /dev/null 87 | if [ $? -eq 0 ]; then 88 | echo "same" 89 | else 90 | # sdiff $TEMPB $TEMP1 91 | echo "changed" 92 | fi 93 | } 94 | 95 | sub_lex_bot() { 96 | if [ -z "$lex_bot_prefix_a" -a -z "$lex_bot_prefix_b" ]; then 97 | cat 98 | else 99 | cat > $TEMP1 100 | local lexExists=$(cat $TEMP1 | jq ".Actions[] | select(.Type == \"ConnectParticipantWithLexBot\") | has(\"Type\")") 101 | if [ -z "$lexExists" ]; then 102 | cat $TEMP1 103 | else 104 | cat $TEMP1 | jq " 105 | .Actions[] | 106 | if .Type == \"ConnectParticipantWithLexBot\" 107 | then .Parameters.LexBot.Name = (.Parameters.LexBot.Name | 108 | sub(\"^$lex_bot_prefix_a\"; \"$lex_bot_prefix_b\")) 109 | else . 110 | end 111 | " > $TEMP2 112 | cat $TEMP1 | 113 | jq -r --slurpfile actions $TEMP2 ".Actions = \$actions" 114 | fi 115 | fi 116 | } 117 | 118 | check_contact_flow() { 119 | cf_json=$1 120 | if [ ! -s $TEMPSED ]; then 121 | cat $helper_sed | 122 | egrep -v "^#" | 123 | cut -d% -f3 > $TEMPSED 124 | fi 125 | echo "### Broken references found in $cf_json:" > $TEMPCHK 126 | cat $cf_json | 127 | jq -r . | 128 | grep arn:aws:connect: | 129 | cut -d\" -f2,4 | 130 | sed -e's/"/ /g' | 131 | egrep -v "^id |^arn " | 132 | while read name val; do 133 | val_id=${val/arn:aws:connect:*\//} 134 | grep -q $val_id $TEMPSED || echo "### $name: $val_id" >> $TEMPCHK 135 | done 136 | if [ $(cat $TEMPCHK | wc -l) -le 1 ]; then 137 | > $TEMPCHK 138 | else 139 | echo >> $TEMPCHK 140 | cat $TEMPCHK 141 | return 1 142 | fi 143 | } 144 | 145 | # AWS CLI needs to output JSON 146 | export AWS_DEFAULT_OUTPUT=json 147 | aws_connect() { 148 | local cmd="" 149 | local ii 150 | for ii; do 151 | [[ "$ii" =~ " " || "$ii" =~ "(" || "$ii" =~ ")" ]] && cmd="$cmd \"$ii\"" || cmd="$cmd $ii" 152 | done 153 | echo "aws connect$profile_flag$cmd" >> "$aws_cli_log" 154 | eval "aws connect$profile_flag$cmd" 2> $TEMPERR 155 | local ret=$? 156 | if [ -s $TEMPERR ]; then 157 | cat $TEMPERR | tee -a "$aws_cli_log" >&2 158 | fi 159 | return $ret 160 | } 161 | 162 | TEMPFILE=$(mktemp) 163 | TEMPERR=${TEMPFILE}_err 164 | TEMPNEW=${TEMPFILE}_new 165 | TEMPOLD=${TEMPFILE}_old 166 | TEMPMOD=${TEMPFILE}_mod 167 | TEMPCHK=${TEMPFILE}_chk 168 | TEMPSED=${TEMPFILE}_sed 169 | TEMPA=${TEMPFILE}_A 170 | TEMPB=${TEMPFILE}_B 171 | TEMP1=${TEMPFILE}_1 172 | TEMP2=${TEMPFILE}_2 173 | TEMPLIST="$TEMPFILE $TEMPERR $TEMPNEW $TEMPOLD $TEMPMOD $TEMPCHK $TEMPSED $TEMPA $TEMPB $TEMP1 $TEMP2" 174 | trap 'rm -r -- $TEMPLIST' EXIT 175 | touch $TEMPLIST 176 | 177 | actionLead="# Action -" 178 | dryrun= 179 | ignore_improper_extended_ascii= 180 | while getopts "?dev" arg; do 181 | case "$arg" in 182 | d) dryrun=on; echo "Dry Run";; 183 | e) ignore_improper_extended_ascii=on;; 184 | v) version;; 185 | *) usage;; 186 | esac 187 | done 188 | shift $((OPTIND-1)) 189 | 190 | if [ "$(hex_code "é")" != "%C3%A9" ]; then 191 | echo "WARNING: This system may not encode Extended ASCII characters properly." >&2 192 | if [ -n "$ignore_improper_extended_ascii" ]; then 193 | echo "Proceed regardless as the -e option is specified." >&2 194 | else 195 | cat <&2 196 | 197 | If your instance component names contain Extended ASCII characters, such as accented letters 198 | like é, this system will encode those names differently from standard encoding. 199 | 200 | If you are sure that your component names do not contain Extended ASCII characters, 201 | you may proceed regardless by running the command again with the -e option. 202 | EOD 203 | exit 1 204 | fi 205 | fi 206 | 207 | helper=$1 208 | 209 | if [ -z "$helper" ]; then 210 | usage 211 | fi 212 | 213 | for ext in var sed new old; do 214 | eval helper_$ext="$helper/helper.$ext" 215 | done 216 | 217 | if [ ! -s "$helper_var" ]; then 218 | error "\"$helper_var\" does not exist. \"$helper\" is probably not a Helper directory." 219 | fi 220 | 221 | . "$helper_var" 222 | profile=$aws_profile_b 223 | profile_flag=${profile:+ --profile $profile} 224 | 225 | cat < "$helper_log" 254 | # This log file is for AWS CLI tracing, not for direct execution. 255 | # Please use the $(basename $0) command (in non-dry-run mode) to perform the copying instead. 256 | # 257 | EOD 258 | 259 | if [ -n "$dryrun" ]; then 260 | cat <> "$helper_log" 261 | # Dry-run mode - Actions listed below are not being carried out 262 | # 263 | EOD 264 | fi 265 | 266 | ############################################################ 267 | # 268 | # Prompts 269 | # 270 | 271 | cat < $TEMPOLD 299 | # Create what is in $helper_new 300 | egrep "^hour_" "$helper_new" > $TEMPNEW 301 | if [ ! -s $TEMPNEW ]; then 302 | echo "No hours of operations to create" 303 | else 304 | num_hours=$(echo $(cat $TEMPNEW | wc -l)) 305 | echo -e "\nCreating $num_hours Hours of operations" 306 | ii=0 307 | sort $TEMPNEW | 308 | while read hour_json; do 309 | ii=$[ii+1] 310 | echo "$ii. $hour_json" 311 | hour_name=${hour_json#hour_} 312 | hour_name=${hour_name%.json} 313 | hour_name_decoded=$(path_decode "$hour_name") 314 | 315 | hour_id_a=$(jq -r ".HoursOfOperation.HoursOfOperationId" "$instance_alias_dir_a/$hour_json" | dos2unix) 316 | 317 | hour_desc=$(jq -r ".HoursOfOperation.Description | select(. != null)" "$instance_alias_dir_a/$hour_json" | dos2unix) 318 | # Description cannot be blank, or the AWS CLI will fail. 319 | if [ -z "$hour_desc" ]; then 320 | hour_desc=$hour_name_decoded 321 | fi 322 | 323 | cat "$instance_alias_dir_a/$hour_json" | 324 | jq --arg iid $instance_id_b --arg desc "$hour_desc" \ 325 | ".HoursOfOperation | del(.HoursOfOperationId, .HoursOfOperationArn) | . + { InstanceId: \$iid} | . + { Description: \$desc}" | 326 | sed -f "$helper_sed" > "$helper/$hour_json" 327 | 328 | cat <> "$helper_log" 329 | 330 | $actionLead Create hours of operation: $hour_name_decoded 331 | EOD 332 | if [ -n "$dryrun" ]; then 333 | cat <> "$helper_log" 339 | aws connect create-hours-of-operation \ 340 | --cli-input-json "file://$helper/$hour_json" \ 341 | > "$helper/output_$hour_json" 342 | EOD 343 | # rm "$helper/$hour_json" 344 | continue 345 | fi 346 | 347 | aws_connect create-hours-of-operation \ 348 | --cli-input-json "file://$helper/$hour_json" \ 349 | > "$helper/output_$hour_json" || error $LINENO 350 | hour_id_b=$(jq -r ".HoursOfOperationId" "$helper/output_$hour_json" | dos2unix) 351 | 352 | aws_connect describe-hours-of-operation \ 353 | --instance-id $instance_id_b \ 354 | --hours-of-operation-id $hour_id_b \ 355 | > "$instance_alias_dir_b/$hour_json" || error $LINENO 356 | 357 | # Moving hour_json from helper_new to helper_old 358 | echo $hour_json >> "$helper_old" 359 | sed -e"/$hour_json/d" "$helper_new" > $TEMPFILE 360 | cat $TEMPFILE > "$helper_new" 361 | 362 | cat <> "$helper_sed" 363 | # Hour of operation: $hour_name_decoded 364 | s%$hour_id_a%$hour_id_b%g 365 | EOD 366 | done 367 | test $? -eq 0 || error 368 | 369 | if [ -z "$dryrun" ]; then 370 | # Update instance B hours 371 | aws_connect list-hours-of-operations \ 372 | --instance-id $instance_id_b \ 373 | > $TEMPFILE || error $LINENO 374 | cat $TEMPFILE | 375 | jq -r ".HoursOfOperationSummaryList[]" > "$instance_alias_dir_b/hours.json" 376 | fi 377 | fi 378 | 379 | if [ ! -s $TEMPOLD ]; then 380 | echo "No hours of operations to update" 381 | else 382 | num_hours=$(echo $(cat $TEMPOLD | wc -l)) 383 | echo -e "\nChecking $num_hours hours of operations for an update" 384 | ii=0 385 | sort $TEMPOLD | 386 | while read hour_json; do 387 | ii=$[ii+1] 388 | echo -n "$ii. $hour_json ... " 389 | hour_name=${hour_json#hour_} 390 | hour_name=${hour_name%.json} 391 | hour_name_decoded=$(path_decode "$hour_name") 392 | cat "$instance_alias_dir_a/$hour_json" > $TEMPA 393 | cat "$instance_alias_dir_b/$hour_json" > $TEMPB 394 | df=$(diff_files); echo $df; test "$df" == "same" && continue 395 | echo "Updating $hour_json" 396 | 397 | hour_id_b=$(jq -r ".HoursOfOperation.HoursOfOperationId" "$instance_alias_dir_b/$hour_json" | dos2unix) 398 | arg_flags=$(cat "$instance_alias_dir_b/$hour_json" | 399 | jq -r ".HoursOfOperation | \"--arg id \" + .HoursOfOperationId" | dos2unix) 400 | cat "$instance_alias_dir_a/$hour_json" | 401 | jq --arg iid $instance_id_b $arg_flags \ 402 | ".HoursOfOperation | del(.HoursOfOperationArn, .Tags) | . + { InstanceId: \$iid, HoursOfOperationId: \$id }" \ 403 | > "$helper/$hour_json" 404 | 405 | cat <> "$helper_log" 406 | 407 | $actionLead Update hours of operation: $hour_name_decoded 408 | EOD 409 | if [ -n "$dryrun" ]; then 410 | cat <> "$helper_log" 416 | aws connect update-hours-of-operation \ 417 | --cli-input-json "file://$helper/$hour_json" \ 418 | > "$helper/output_$hour_json" 419 | EOD 420 | # rm "$helper/$hour_json" 421 | continue 422 | fi 423 | 424 | aws_connect update-hours-of-operation \ 425 | --cli-input-json "file://$helper/$hour_json" \ 426 | > "$helper/output_$hour_json" || error $LINENO 427 | 428 | aws_connect describe-hours-of-operation \ 429 | --instance-id $instance_id_b \ 430 | --hours-of-operation-id $hour_id_b \ 431 | > "$instance_alias_dir_b/$hour_json" || error $LINENO 432 | done 433 | test $? -eq 0 || error 434 | fi 435 | 436 | 437 | ############################################################ 438 | # 439 | # Queues 440 | # 441 | 442 | cat < $TEMPOLD 449 | # Create what is in $helper_new 450 | egrep "^queue_" "$helper_new" > $TEMPNEW 451 | if [ ! -s $TEMPNEW ]; then 452 | echo "No queues to create" 453 | else 454 | num_queues=$(echo $(cat $TEMPNEW | wc -l)) 455 | echo -e "\nCreating $num_queues queues" 456 | ii=0 457 | sort $TEMPNEW | 458 | while read queue_json; do 459 | ii=$[ii+1] 460 | echo "$ii. $queue_json" 461 | queue_name=${queue_json#queue_} 462 | queue_name=${queue_name%.json} 463 | queue_name_decoded=$(path_decode "$queue_name") 464 | 465 | queue_id_a=$(jq -r ".Queue.QueueId" "$instance_alias_dir_a/$queue_json" | dos2unix) 466 | 467 | cat "$instance_alias_dir_a/$queue_json" | 468 | jq --arg instance_id $instance_id_b \ 469 | ".Queue | del(.QueueId, .QueueArn, .OutboundCallerConfig, .Status) | . + { InstanceId: \$instance_id}" | 470 | sed -f "$helper_sed" > "$helper/$queue_json" 471 | 472 | cat <> "$helper_log" 473 | 474 | $actionLead Create queue: $queue_name_decoded 475 | EOD 476 | if [ -n "$dryrun" ]; then 477 | cat <> "$helper_log" 483 | aws connect create-queue \ 484 | --cli-input-json "file://$helper/$queue_json" \ 485 | > "$helper/output_$queue_json" 486 | EOD 487 | # rm "$helper/$queue_json" 488 | continue 489 | fi 490 | 491 | aws_connect create-queue \ 492 | --cli-input-json "file://$helper/$queue_json" \ 493 | > "$helper/output_$queue_json" || error $LINENO 494 | queue_id_b=$(jq -r ".QueueId" "$helper/output_$queue_json" | dos2unix) 495 | 496 | aws_connect describe-queue \ 497 | --instance-id $instance_id_b \ 498 | --queue-id $queue_id_b \ 499 | > "$instance_alias_dir_b/$queue_json" || error $LINENO 500 | 501 | # Moving queue_json from helper_new to helper_old 502 | echo $queue_json >> "$helper_old" 503 | sed -e"/$queue_json/d" "$helper_new" > $TEMPFILE 504 | cat $TEMPFILE > "$helper_new" 505 | 506 | cat <> "$helper_sed" 507 | # Queue: $queue_name_decoded 508 | s%$queue_id_a%$queue_id_b%g 509 | EOD 510 | done 511 | test $? -eq 0 || error 512 | 513 | if [ -z "$dryrun" ]; then 514 | # Update instance B queues 515 | aws_connect list-queues \ 516 | --instance-id $instance_id_b \ 517 | --queue-types "STANDARD" \ 518 | > $TEMPFILE || error $LINENO 519 | cat $TEMPFILE | 520 | jq -r ".QueueSummaryList[] | select(.QueueType != \"AGENT\")" > "$instance_alias_dir_b/queues.json" 521 | fi 522 | fi 523 | 524 | if [ ! -s $TEMPOLD ]; then 525 | echo "No queues to update" 526 | else 527 | num_queues=$(echo $(cat $TEMPOLD | wc -l)) 528 | echo -e "\nChecking $num_queues queues for an update" 529 | ii=0 530 | sort $TEMPOLD | 531 | while read queue_json; do 532 | ii=$[ii+1] 533 | echo -n "$ii. $queue_json ... " 534 | cat "$instance_alias_dir_a/$queue_json" | 535 | jq "del(.Queue.OutboundCallerConfig)" > $TEMPA 536 | cat "$instance_alias_dir_b/$queue_json" > $TEMPB 537 | df=$(diff_files); echo $df; test "$df" == "same" && continue 538 | echo "Please update $queue_json manually considering potential operation impact." 539 | done 540 | fi 541 | 542 | 543 | ############################################################ 544 | # 545 | # Routing Profiles 546 | # 547 | 548 | gen_helper_rpqr() { 549 | # Routing Profile Queue Reference 550 | routing_name=$1 551 | out_file="$helper/routingQRs_$routing_name.json" 552 | cat "$instance_alias_dir_a/routingQs_$routing_name.json" | 553 | jq ".RoutingProfileQueueConfigSummaryList[] | 554 | { QueueReference: { QueueId, Channel }, Priority, Delay }" \ 555 | > "$out_file" 556 | echo "$out_file" 557 | } 558 | 559 | gen_helper_routing_new() { 560 | # Must not have QueueConfigs in creation, leave it to association to handle >10 cases 561 | routing_name=$1 562 | out_file="$helper/routingNew_$routing_name.json" 563 | cat "$instance_alias_dir_a/routing_$routing_name.json" | 564 | jq --arg iid $instance_id_b \ 565 | ".RoutingProfile | 566 | del(.RoutingProfileId, .RoutingProfileArn) | 567 | . + { InstanceId: \$iid } | 568 | del(.MediaConcurrencies[] | select(.Concurrency == 0))" | 569 | sed -f "$helper_sed" > "$out_file" 570 | echo "$out_file" 571 | } 572 | 573 | cat < $TEMPOLD 580 | # Create what is in $helper_new 581 | egrep "^routing_" "$helper_new" > $TEMPNEW 582 | if [ ! -s $TEMPNEW ]; then 583 | echo "No routing profiles to create" 584 | else 585 | num_routings=$(echo $(cat $TEMPNEW | wc -l)) 586 | echo -e "\nCreating $num_routings routing profiles" 587 | ii=0 588 | sort $TEMPNEW | 589 | while read routing_json; do 590 | ii=$[ii+1] 591 | echo "$ii. $routing_json" 592 | routing_name=${routing_json#routing_} 593 | routing_name=${routing_name%.json} 594 | routing_name_decoded=$(path_decode "$routing_name") 595 | 596 | routing_id_a=$(jq -r ".RoutingProfile.RoutingProfileId" "$instance_alias_dir_a/$routing_json" | dos2unix) 597 | routing_new_file=$(gen_helper_routing_new "$routing_name" | dos2unix) 598 | out_file="$helper/output_routing_new_$routing_name.json" 599 | 600 | cat <> "$helper_log" 601 | 602 | $actionLead Create routing profile: $routing_name_decoded 603 | EOD 604 | if [ -n "$dryrun" ]; then 605 | cat <> "$helper_log" 611 | aws connect create-routing-profile \ 612 | --cli-input-json "file://$routing_new_file" \ 613 | > "$out_file" 614 | EOD 615 | # rm "$routing_new_file" 616 | continue 617 | fi 618 | 619 | aws_connect create-routing-profile \ 620 | --cli-input-json "file://$routing_new_file" \ 621 | > "$out_file" || error $LINENO 622 | routing_id_b=$(jq -r ".RoutingProfileId" "$out_file" | dos2unix) 623 | 624 | # All routing profiles will be updated with queues 625 | echo "$routing_json" >> $TEMPOLD 626 | 627 | # Update instance B (even it is not final) to allow comparison in update 628 | aws_connect describe-routing-profile \ 629 | --instance-id $instance_id_b \ 630 | --routing-profile-id $routing_id_b | 631 | jq -r "del(.RoutingProfile.NumberOfAssociatedQueues, .RoutingProfile.NumberOfAssociatedUsers)" \ 632 | > "$instance_alias_dir_b/$routing_json" || error $LINENO 633 | 634 | aws_connect list-routing-profile-queues \ 635 | --instance-id $instance_id_b \ 636 | --routing-profile-id $routing_id_b \ 637 | > "$instance_alias_dir_b/routingQs_$routing_name.json" || error $LINENO 638 | 639 | # Moving routing_json from helper_new to helper_old 640 | echo "$routing_json" >> "$helper_old" 641 | sed -e"/$routing_json/d" "$helper_new" > $TEMPFILE 642 | cat $TEMPFILE > "$helper_new" 643 | 644 | cat <> "$helper_sed" 645 | # Routing Profile: $routing_name_decoded 646 | s%$routing_id_a%$routing_id_b%g 647 | EOD 648 | done 649 | test $? -eq 0 || error 650 | 651 | if [ -z "$dryrun" ]; then 652 | # Update instance B routing profiles 653 | aws_connect list-routing-profiles \ 654 | --instance-id $instance_id_b \ 655 | > $TEMPFILE || error $LINENO 656 | cat $TEMPFILE | 657 | jq -r ".RoutingProfileSummaryList[]" > "$instance_alias_dir_b/routings.json" 658 | fi 659 | fi 660 | 661 | if [ ! -s $TEMPOLD ]; then 662 | echo "No routing profiles to update" 663 | else 664 | num_routings=$(echo $(cat $TEMPOLD | wc -l)) 665 | # echo $num_routings existing routing profiles: not to be auto-updated due to possible operation impact 666 | echo -e "\nChecking $num_routings routing profiles for an update" 667 | ii=0 668 | sort $TEMPOLD | 669 | while read routing_json; do 670 | ii=$[ii+1] 671 | echo -n "$ii. $routing_json ... " 672 | routing_name=${routing_json#routing_} 673 | routing_name=${routing_name%.json} 674 | routing_name_decoded=$(path_decode "$routing_name") 675 | 676 | cat "$instance_alias_dir_a/$routing_json" | 677 | jq "del(.RoutingProfile.MediaConcurrencies[] | select(.Concurrency == 0))" > $TEMPA 678 | cat "$instance_alias_dir_b/$routing_json" | 679 | jq "del(.RoutingProfile.MediaConcurrencies[] | select(.Concurrency == 0))" > $TEMPB 680 | # df=$(diff_files); echo $df; test "$df" == "same" && continue 681 | df_r=$(diff_files) 682 | 683 | cat "$instance_alias_dir_a/routingQs_$routing_name.json" > $TEMPA 684 | cat "$instance_alias_dir_b/routingQs_$routing_name.json" > $TEMPB 685 | # df=$(diff_files); echo $df; test "$df" == "same" && continue 686 | df_rq=$(diff_files) 687 | 688 | if [ "$df_r" == "same" -a "$df_rq" == "same" ]; then 689 | echo same 690 | continue 691 | fi 692 | 693 | echo " routing $df_r routing-queues $df_rq" 694 | echo "Updating $routing_json" 695 | 696 | # routing_old_file=$(gen_helper_routing_old "$routing_name") 697 | routing_id_b=$(jq -r ".RoutingProfile.RoutingProfileId" "$instance_alias_dir_b/$routing_json" | dos2unix) 698 | 699 | if [ "$df_r" != "same" ]; then 700 | # Update Routing Profile of Instance B ahead of time 701 | # then update the concurrency and default-outbound-queue. 702 | # (The name must have already matched.) 703 | if [ -z "$dryrun" ]; then 704 | cat "$instance_alias_dir_a/$routing_json" | 705 | sed -f "$helper_sed" > "$instance_alias_dir_b/$routing_json" 706 | fi 707 | 708 | routing_doq_b=$(cat "$instance_alias_dir_b/$routing_json" | 709 | jq -r ".RoutingProfile.DefaultOutboundQueueId" | 710 | dos2unix) 711 | 712 | cat "$instance_alias_dir_b/$routing_json" | 713 | jq -r ".RoutingProfile.MediaConcurrencies[] | select(.Concurrency != 0)" | 714 | jq -s "." > "$helper/routingConcurrency_$routing_name.json" 715 | 716 | cat <> "$helper_log" 717 | 718 | $actionLead Update routing profile: $routing_decoded 719 | EOD 720 | if [ -n "$dryrun" ]; then 721 | cat <> "$helper_log" 728 | aws connect update-routing-profile-default-outbound-queue \ 729 | --instance-id $instance_id_b \ 730 | --routing-profile-id $routing_id_b \ 731 | --default-outbound-queue-id $routing_doq_b 732 | aws connect update-routing-profile-concurrency \ 733 | --instance-id $instance_id_b \ 734 | --routing-profile-id $routing_id_b \ 735 | --media-concurrencies "file://$helper/routingConcurrency_$routing_name.json" 736 | EOD 737 | # rm "$helper/routingConcurrency_$routing_name.json" 738 | else 739 | aws_connect update-routing-profile-default-outbound-queue \ 740 | --instance-id $instance_id_b \ 741 | --routing-profile-id $routing_id_b \ 742 | --default-outbound-queue-id $routing_doq_b || error $LINENO 743 | 744 | aws_connect update-routing-profile-concurrency \ 745 | --instance-id $instance_id_b \ 746 | --routing-profile-id $routing_id_b \ 747 | --media-concurrencies "file://$helper/routingConcurrency_$routing_name.json" || error $LINENO 748 | 749 | aws_connect describe-routing-profile \ 750 | --instance-id $instance_id_b \ 751 | --routing-profile-id $routing_id_b | 752 | jq -r "del(.RoutingProfile.NumberOfAssociatedQueues, .RoutingProfile.NumberOfAssociatedUsers)" \ 753 | > "$instance_alias_dir_b/$routing_json" || error $LINENO 754 | fi 755 | fi 756 | 757 | if [ "$df_rq" != "same" ]; then 758 | 759 | cat "$instance_alias_dir_a/routingQs_$routing_name.json" | 760 | jq -r ".RoutingProfileQueueConfigSummaryList[].QueueName" | 761 | sort -u > $TEMPA 762 | cat "$instance_alias_dir_b/routingQs_$routing_name.json" | 763 | jq -r ".RoutingProfileQueueConfigSummaryList[].QueueName" | 764 | sort -u > $TEMPB 765 | > $TEMP1 766 | diff $TEMPA $TEMPB | grep "^< " | sed -e"s/^< //" > $TEMP2 767 | if [ -s $TEMP2 ]; then 768 | helper_rqc_json="$helper/routingQueueConfig_$routing_name.json" 769 | cat $TEMP2 | 770 | while read q_name; do 771 | # cat "$instance_alias_dir_a/routingQs_$routing_name.json" | 772 | # jq -r ".RoutingProfileQueueConfigSummaryList[] | select(.QueueName == \"${q_name//\"/\\\"}\") | { QueueReference: { QueueId, Channel }, Priority, Delay }" >> $TEMP1 773 | sed -e's/\\"/%22/g' "$instance_alias_dir_a/routingQs_$routing_name.json" | 774 | jq -r ".RoutingProfileQueueConfigSummaryList[] | select(.QueueName == \"${q_name//\"/%22}\") | { QueueReference: { QueueId, Channel }, Priority, Delay }" >> $TEMP1 775 | done 776 | 777 | cat $TEMP1 | sed -f "$helper_sed" | jq -s "." > "$helper_rqc_json" 778 | 779 | cat <> "$helper_log" 780 | 781 | $actionLead Associate queues to routing profile: $routing_name_decoded 782 | EOD 783 | num_queue_configs=$(cat "$helper_rqc_json" | jq "length") 784 | if [ -n "$dryrun" ]; then 785 | cat <> "$helper_log" 793 | fi 794 | cat <> "$helper_log" 795 | aws connect associate-routing-profile-queues \ 796 | --instance-id $instance_id_b \ 797 | --routing-profile-id $routing_id_b \ 798 | --queue-configs "file://$helper_rqc_json" 799 | EOD 800 | # rm "$helper_rqc_json" 801 | else 802 | # cat $TEMP2 803 | # aws_connect associate-routing-profile-queues \ 804 | # --instance-id $instance_id_b \ 805 | # --routing-profile-id $routing_id_b \ 806 | # --queue-configs "file://$helper_rqc_json" || error $LINENO 807 | # echo $num_queue_configs Queue Configs 808 | iii=0 809 | while [ "$iii" -lt "$num_queue_configs" ]; do 810 | jjj=$[iii+AWS_CLI_MAX_QUEUE_CONFIGS] 811 | arg_queue_configs="'$(cat "$helper_rqc_json" | jq ".[$iii:$jjj]")'" 812 | aws_connect associate-routing-profile-queues \ 813 | --instance-id $instance_id_b \ 814 | --routing-profile-id $routing_id_b \ 815 | --queue-configs $arg_queue_configs || error $LINENO 816 | iii=$jjj 817 | done 818 | fi 819 | fi 820 | 821 | if [ -z "$dryrun" ]; then 822 | aws_connect list-routing-profile-queues \ 823 | --instance-id $instance_id_b \ 824 | --routing-profile-id $routing_id_b \ 825 | > "$instance_alias_dir_b/routingQs_$routing_name.json" || error $LINENO 826 | fi 827 | fi 828 | done 829 | test $? -eq 0 || error 830 | fi 831 | 832 | 833 | ############################################################ 834 | # 835 | # Contact Flow Modules Creation 836 | # TODO: 837 | # - Use $helper/module_template.json as a template to create new contact flows 838 | # 839 | # Note: Must create all flow modules and flows from an empty template first 840 | # such that references can be resolved in updates 841 | # 842 | 843 | cat < $TEMPMOD 852 | # Create what is in $helper_new 853 | egrep "^module_$contact_flow_prefix_encoded" "$helper_new" > $TEMPNEW 854 | if [ ! -s $TEMPNEW ]; then 855 | echo "No contact flow modules$contact_flow_prefix_text to create" 856 | else 857 | num_modules=$(echo $(cat $TEMPNEW | wc -l)) 858 | echo -e "\nCreating $num_modules contact flow modules$contact_flow_prefix_text" 859 | ii=0 860 | sort $TEMPNEW | 861 | while read module_json; do 862 | ii=$[ii+1] 863 | echo "$ii. $module_json" 864 | module_name=${module_json#module_} 865 | module_name=${module_name%.json} 866 | module_name_decoded=$(path_decode "$module_name") 867 | 868 | # module_id_a=$(jq -r "select(.Name == \"${module_name_decoded//\"/\\\"}\") | .Id" "$instance_alias_dir_a/modules.json") 869 | module_id_a=$(sed -e's/\\"/%22/g' "$instance_alias_dir_a/modules.json" | jq -r "select(.Name == \"${module_name_decoded//\"/%22}\") | .Id" | dos2unix) 870 | out_file="$helper/output_$module_json" 871 | 872 | # Contact Flow Module template file 873 | module_template_file=$helper/module_template.json 874 | 875 | # cat "$instance_alias_dir_a/$module_json" | 876 | # sed -f "$helper_sed" | 877 | # sub_lex_bot \ 878 | # > "$helper/$module_json" 879 | 880 | cat <> "$helper_log" 881 | 882 | $actionLead Create contact flow module: $module_name_decoded 883 | EOD 884 | if [ -n "$dryrun" ]; then 885 | cat <> "$helper_log" 892 | aws connect create-contact-flow-module \ 893 | --instance-id $instance_id_b \ 894 | --name "${module_name_decoded//\"/\\\"}" \ 895 | --content "file://$module_template_file" \ 896 | > "$out_file" 897 | EOD 898 | # rm "$helper/$module_json" 899 | continue 900 | fi 901 | 902 | aws_connect create-contact-flow-module \ 903 | --instance-id $instance_id_b \ 904 | --name "${module_name_decoded//\"/\\\"}" \ 905 | --content "file://$module_template_file" \ 906 | > "$out_file" || error $LINENO 907 | module_id_b=$(jq -r ".Id" "$out_file" | dos2unix) 908 | 909 | # All flow modules will be updated with content 910 | echo "$module_json" >> $TEMPMOD 911 | 912 | aws_connect describe-contact-flow-module \ 913 | --instance-id $instance_id_b \ 914 | --contact-flow-module-id $module_id_b \ 915 | > $TEMPFILE || error $LINENO 916 | cat $TEMPFILE | 917 | jq -r '.ContactFlowModule.Content' > "$instance_alias_dir_b/$module_json" 918 | 919 | # Moving module_json from helper_new to helper_old 920 | echo "$module_json" >> "$helper_old" 921 | sed -e"/$module_json/d" "$helper_new" > $TEMPFILE 922 | cat $TEMPFILE > "$helper_new" 923 | 924 | cat <> "$helper_sed" 925 | # Contact Flow Module: $module_name_decoded 926 | s%$module_id_a%$module_id_b%g 927 | EOD 928 | done 929 | test $? -eq 0 || error 930 | 931 | if [ -z "$dryrun" ]; then 932 | # Update instance B flow modules 933 | aws_connect list-contact-flow-modules \ 934 | --instance-id $instance_id_b \ 935 | > $TEMPFILE || error $LINENO 936 | cat $TEMPFILE | 937 | jq -r ".ContactFlowModulesSummaryList[] | select(.Name | test(\"^($contact_flow_prefix|Default ).*\"))" \ 938 | > "$instance_alias_dir_b/modules.json" 939 | fi 940 | fi 941 | 942 | 943 | ############################################################ 944 | # 945 | # Contact Flows Creation 946 | # 947 | # TODO: 948 | # - Use $helper/flow_template.json as a template to create new contact flows 949 | # - Use the Default special type contact flow to create special type contact flows 950 | # - Skip "Default " and "Sample " contact flows 951 | 952 | cat < $TEMPOLD 960 | # Create what is in $helper_new 961 | egrep "^flow_$contact_flow_prefix_encoded" "$helper_new" > $TEMPNEW 962 | if [ ! -s $TEMPNEW ]; then 963 | echo "No contact flows$contact_flow_prefix_text to create" 964 | else 965 | num_flows=$(echo $(cat $TEMPNEW | wc -l)) 966 | echo -e "\nCreating $num_flows contact flows$contact_flow_prefix_text" 967 | ii=0 968 | sort $TEMPNEW | 969 | while read flow_json; do 970 | ii=$[ii+1] 971 | echo "$ii. $flow_json" 972 | flow_name=${flow_json#flow_} 973 | flow_name=${flow_name%.json} 974 | flow_name_decoded=$(path_decode "$flow_name") 975 | # flow_type=$(jq -r "select(.Name == \"${flow_name_decoded//\"/\\\"}\") | .ContactFlowType" "$instance_alias_dir_a/flows.json") 976 | flow_type=$(sed -e's/\\"/%22/g' "$instance_alias_dir_a/flows.json" | jq -r "select(.Name == \"${flow_name_decoded//\"/%22}\") | .ContactFlowType" | dos2unix) 977 | # flow_id_a=$(jq -r "select(.Name == \"${flow_name_decoded//\"/\\\"}\") | .Id" "$instance_alias_dir_a/flows.json") 978 | flow_id_a=$(sed -e's/\\"/%22/g' "$instance_alias_dir_a/flows.json" | jq -r "select(.Name == \"${flow_name_decoded//\"/%22}\") | .Id" | dos2unix) 979 | out_file="$helper/output_$flow_json" 980 | 981 | # Contact Flow template file 982 | flow_template_file=$helper/flow_template.json 983 | case "$flow_type" in 984 | CUSTOMER_QUEUE) 985 | flow_template_file="$instance_alias_dir_b/flow_Default%20customer%20queue.json";; 986 | CUSTOMER_HOLD) 987 | flow_template_file="$instance_alias_dir_b/flow_Default%20customer%20hold.json";; 988 | CUSTOMER_WHISPER) 989 | flow_template_file="$instance_alias_dir_b/flow_Default%20customer%20whisper.json";; 990 | AGENT_HOLD) 991 | flow_template_file="$instance_alias_dir_b/flow_Default%20agent%20hold.json";; 992 | AGENT_WHISPER) 993 | flow_template_file="$instance_alias_dir_b/flow_Default%20agent%20whisper.json";; 994 | OUTBOUND_WHISPER) 995 | flow_template_file="$instance_alias_dir_b/flow_Default%20outbound.json";; 996 | AGENT_TRANSFER) 997 | flow_template_file="$instance_alias_dir_b/flow_Default%20agent%20transfer.json";; 998 | QUEUE_TRANSFER) 999 | flow_template_file="$instance_alias_dir_b/flow_Default%20queue%20transfer.json";; 1000 | esac 1001 | 1002 | cat <> "$helper_log" 1003 | 1004 | $actionLead Create contact flow: $flow_name_decoded 1005 | EOD 1006 | if [ -n "$dryrun" ]; then 1007 | cat <> "$helper_log" 1015 | aws connect create-contact-flow \ 1016 | --instance-id $instance_id_b \ 1017 | --name "${flow_name_decoded//\"/\\\"}" \ 1018 | --type $flow_type \ 1019 | --content "file://$flow_template_file" \ 1020 | > "$out_file" 1021 | EOD 1022 | continue 1023 | fi 1024 | 1025 | aws_connect create-contact-flow \ 1026 | --instance-id $instance_id_b \ 1027 | --name "${flow_name_decoded//\"/\\\"}" \ 1028 | --type $flow_type \ 1029 | --content "file://$flow_template_file" \ 1030 | > "$out_file" || error $LINENO 1031 | flow_id_b=$(jq -r ".ContactFlowId" "$out_file" | dos2unix) 1032 | 1033 | # All flows will be updated with content 1034 | echo "$flow_json" >> $TEMPOLD 1035 | 1036 | aws_connect describe-contact-flow \ 1037 | --instance-id $instance_id_b \ 1038 | --contact-flow-id $flow_id_b \ 1039 | > $TEMPFILE || error $LINENO 1040 | cat $TEMPFILE | 1041 | jq -r '.ContactFlow.Content' > "$instance_alias_dir_b/$flow_json" 1042 | 1043 | # Moving flow_json from helper_new to helper_old 1044 | echo "$flow_json" >> "$helper_old" 1045 | sed -e"/$flow_json/d" "$helper_new" > $TEMPFILE 1046 | cat $TEMPFILE > "$helper_new" 1047 | 1048 | cat <> "$helper_sed" 1049 | # Contact Flow: $flow_name_decoded 1050 | s%$flow_id_a%$flow_id_b%g 1051 | EOD 1052 | done 1053 | test $? -eq 0 || error 1054 | 1055 | if [ -z "$dryrun" ]; then 1056 | # Update instance B flows 1057 | aws_connect list-contact-flows \ 1058 | --instance-id $instance_id_b \ 1059 | > $TEMPFILE || error $LINENO 1060 | cat $TEMPFILE | 1061 | jq -r ".ContactFlowSummaryList[] | select(.Name | test(\"^($contact_flow_prefix|Default ).*\"))" \ 1062 | > "$instance_alias_dir_b/flows.json" 1063 | fi 1064 | fi 1065 | 1066 | 1067 | ############################################################ 1068 | # 1069 | # Contact Flow Modules Update 1070 | # 1071 | 1072 | cat < $TEMPA 1101 | cat "$instance_alias_dir_b/$module_json" > $TEMPB 1102 | df=$(diff_files); echo $df; test "$df" == "same" && continue 1103 | 1104 | cat "$instance_alias_dir_a/$module_json" | 1105 | sed -f "$helper_sed" | 1106 | sub_lex_bot \ 1107 | > "$helper/$module_json" 1108 | 1109 | cat <> "$helper_log" 1110 | 1111 | $actionLead Update contact flow module: $module_name_decoded 1112 | EOD 1113 | if [ -n "$dryrun" ]; then 1114 | cat <> "$helper_log" 1121 | cat <> "$helper_log" 1122 | aws connect update-contact-flow-module-content \ 1123 | --instance-id $instance_id_b \ 1124 | --contact-flow-module-id $module_id_b \ 1125 | --content "file://$helper/$module_json" \ 1126 | > "$helper/output_content_$module_json" 1127 | aws connect update-contact-flow-module-metadata \ 1128 | --instance-id $instance_id_b \ 1129 | --contact-flow-module-id $module_id_b \ 1130 | --description "${module_desc//\"/\\\"}" \ 1131 | >> "$helper/output_content_$module_json" 1132 | EOD 1133 | # rm "$helper/$module_json" 1134 | continue 1135 | fi 1136 | 1137 | check_contact_flow $helper/$module_json 1138 | if [ $? -ne 0 ]; then 1139 | cat $TEMPCHK >> "$helper_log" 1140 | # Continue to handle error 1141 | fi 1142 | 1143 | aws_connect update-contact-flow-module-content \ 1144 | --instance-id $instance_id_b \ 1145 | --contact-flow-module-id $module_id_b \ 1146 | --content "file://$helper/$module_json" \ 1147 | > "$helper/output_content_$module_json" || error $LINENO 1148 | 1149 | aws_connect update-contact-flow-module-metadata \ 1150 | --instance-id $instance_id_b \ 1151 | --contact-flow-module-id $module_id_b \ 1152 | --description "${module_desc//\"/\\\"}" \ 1153 | >> "$helper/output_content_$module_json" || error $LINENO 1154 | 1155 | aws_connect describe-contact-flow-module \ 1156 | --instance-id $instance_id_b \ 1157 | --contact-flow-module-id $module_id_b \ 1158 | > $TEMPFILE || error $LINENO 1159 | cat $TEMPFILE | 1160 | jq -r '.ContactFlowModule.Content' > "$instance_alias_dir_b/$module_json" 1161 | done 1162 | test $? -eq 0 || error 1163 | fi 1164 | 1165 | 1166 | ############################################################ 1167 | # 1168 | # Contact Flows Update 1169 | # 1170 | 1171 | cat < $TEMPA 1199 | cat "$instance_alias_dir_b/$flow_json" > $TEMPB 1200 | df=$(diff_files); echo $df; test "$df" == "same" && continue 1201 | 1202 | cat "$instance_alias_dir_a/$flow_json" | 1203 | sed -f "$helper_sed" | 1204 | sub_lex_bot \ 1205 | > "$helper/$flow_json" 1206 | 1207 | cat <> "$helper_log" 1208 | 1209 | $actionLead Update contact flow: $flow_name_decoded 1210 | EOD 1211 | if [ -n "$dryrun" ]; then 1212 | cat <> "$helper_log" 1219 | cat <> "$helper_log" 1220 | aws connect update-contact-flow-content \ 1221 | --instance-id $instance_id_b \ 1222 | --contact-flow-id $flow_id_b \ 1223 | --content "file://$helper/$flow_json" \ 1224 | > "$helper/output_content_$flow_json" 1225 | aws connect update-contact-flow-metadata \ 1226 | --instance-id $instance_id_b \ 1227 | --contact-flow-id $flow_id_b \ 1228 | --description "${flow_desc//\"/\\\"}" \ 1229 | >> "$helper/output_content_$flow_json" 1230 | EOD 1231 | # rm "$helper/$flow_json" 1232 | continue 1233 | fi 1234 | 1235 | check_contact_flow $helper/$flow_json 1236 | if [ $? -ne 0 ]; then 1237 | cat $TEMPCHK >> "$helper_log" 1238 | # Continue to handle error 1239 | fi 1240 | 1241 | aws_connect update-contact-flow-content \ 1242 | --instance-id $instance_id_b \ 1243 | --contact-flow-id $flow_id_b \ 1244 | --content "file://$helper/$flow_json" \ 1245 | > "$helper/output_content_$flow_json" || error $LINENO 1246 | 1247 | aws_connect update-contact-flow-metadata \ 1248 | --instance-id $instance_id_b \ 1249 | --contact-flow-id $flow_id_b \ 1250 | --description "${flow_desc//\"/\\\"}" \ 1251 | >> "$helper/output_content_$flow_json" || error $LINENO 1252 | 1253 | aws_connect describe-contact-flow \ 1254 | --instance-id $instance_id_b \ 1255 | --contact-flow-id $flow_id_b \ 1256 | > $TEMPFILE || error $LINENO 1257 | cat $TEMPFILE | 1258 | jq -r '.ContactFlow.Content' > "$instance_alias_dir_b/$flow_json" 1259 | done 1260 | test $? -eq 0 || error 1261 | fi 1262 | 1263 | 1264 | ############################################################ 1265 | # 1266 | # Contact flows and modules association with Lambda functions 1267 | # 1268 | 1269 | lambdaArnLead=arn:aws:lambda:$region_b:$aws_ac_b:function: 1270 | cat \ 1271 | "$instance_alias_dir_b/flow_${contact_flow_prefix_encoded}"* \ 1272 | "$instance_alias_dir_b/module_${contact_flow_prefix_encoded}"* 2> /dev/null | 1273 | jq -r ".Actions[] | select(.Type == \"InvokeLambdaFunction\") | .Parameters.LambdaFunctionARN" | 1274 | grep "$lambdaArnLead" | 1275 | sort -u > $TEMPFILE 1276 | 1277 | if [ -s $TEMPFILE ]; then 1278 | echo 1279 | echo Associating Lambda functions to $instance_alias_b 1280 | # Use "aws connect" instead of "aws_connect" to avoid logging 1281 | aws connect list-lambda-functions \ 1282 | $profile_flag \ 1283 | --instance-id $instance_id_b \ 1284 | > $TEMPOLD || error $LINENO 1285 | cat $TEMPOLD | 1286 | jq -r ".LambdaFunctions" > "$helper/lambdas.json" 1287 | 1288 | ii=0 1289 | cat $TEMPFILE | 1290 | while read lambdaArn; do 1291 | ii=$[ii+1] 1292 | echo -n "$ii. $lambdaArn ... " 1293 | lambdaExists=$(echo $(cat "$helper/lambdas.json" | jq ".[] | select(. == \"$lambdaArn\")" | wc -l)) 1294 | if [ "$lambdaExists" -gt 0 ]; then 1295 | echo "already associated" 1296 | continue 1297 | fi 1298 | echo "to be associated" 1299 | 1300 | cat <> "$helper_log" 1301 | 1302 | $actionLead Associate Lambda function: $lambdaArn 1303 | EOD 1304 | if [ -n "$dryrun" ]; then 1305 | cat <> "$helper_log" 1313 | 1314 | aws connect associate-lambda-function \ 1315 | --instance-id $instance_id_b \ 1316 | --function-arn $lambdaArn 1317 | EOD 1318 | continue 1319 | fi 1320 | aws_connect associate-lambda-function \ 1321 | --instance-id $instance_id_b \ 1322 | --function-arn $lambdaArn || error $LINENO 1323 | done 1324 | fi 1325 | 1326 | 1327 | ############################################################ 1328 | # 1329 | # Contact flows and modules association with Lex bots 1330 | # 1331 | instance_alias_dir_to_discover=$instance_alias_dir_b 1332 | if [ -n "$dryrun" ]; then 1333 | instance_alias_dir_to_discover=$instance_alias_dir_a 1334 | fi 1335 | 1336 | cat \ 1337 | "$instance_alias_dir_to_discover/flow_${contact_flow_prefix_encoded}"* \ 1338 | "$instance_alias_dir_to_discover/module_${contact_flow_prefix_encoded}"* 2> /dev/null | 1339 | jq -r ".Actions[] | 1340 | select(.Type == \"ConnectParticipantWithLexBot\") | 1341 | .Parameters.LexBot | 1342 | select(. != null) | 1343 | \"Name=\" + .Name + \",LexRegion=\" + .Region" | 1344 | sort -u > $TEMPFILE 1345 | 1346 | if [ -s $TEMPFILE ]; then 1347 | echo 1348 | echo "Associating Lex bots (Classic) to $instance_alias_b" 1349 | cat $TEMPFILE 1350 | # Use "aws connect" instead of "aws_connect" to avoid logging 1351 | aws connect list-lex-bots \ 1352 | $profile_flag \ 1353 | --instance-id $instance_id_b \ 1354 | > $TEMPOLD || error $LINENO 1355 | cat $TEMPOLD | 1356 | jq -r ".LexBots" > "$helper/lex-bots.json" 1357 | 1358 | ii=0 1359 | cat $TEMPFILE | 1360 | while read lexBot; do 1361 | ii=$[ii+1] 1362 | echo -n "$ii. $lexBot ... " 1363 | lexBotName=${lexBot%%,*} 1364 | lexBotName=${lexBotName#Name=} 1365 | lexBotExists=$(echo $(cat "$helper/lex-bots.json" | jq ".[] | select(.Name == \"$lexBotName\")" | wc -l)) 1366 | if [ "$lexBotExists" -gt 0 ]; then 1367 | echo "already associated" 1368 | continue 1369 | fi 1370 | echo "to be associated" 1371 | 1372 | cat <> "$helper_log" 1373 | 1374 | $actionLead Associate Lex Bot (Classic): $lexBot 1375 | EOD 1376 | if [ -n "$dryrun" ]; then 1377 | if [ -n "$lex_bot_prefix_a" -o -n "$lex_bot_prefix_b" ]; then 1378 | lexBot=$(echo "$lexBot" | sed -e"s/^Name=$lex_bot_prefix_a/Name=$lex_bot_prefix_b/") 1379 | fi 1380 | cat <> "$helper_log" 1388 | 1389 | aws connect associate-lex-bot \ 1390 | --instance-id $instance_id_b \ 1391 | --lex-bot $lexBot 1392 | EOD 1393 | continue 1394 | fi 1395 | aws_connect associate-lex-bot \ 1396 | --instance-id $instance_id_b \ 1397 | --lex-bot $lexBot || error $LINENO 1398 | done 1399 | fi 1400 | 1401 | 1402 | ############################################################ 1403 | # 1404 | # Conclusion 1405 | # 1406 | 1407 | num_actions=$(echo $(egrep "^$actionLead" "$helper_log" | wc -l)) 1408 | echo 1409 | echo "$num_actions actions on the target instance" 1410 | if [ "$num_actions" -eq 0 ]; then 1411 | echo "Target instance is the same as the source instance. No updates are required." 1412 | exit 3 1413 | else 1414 | echo 1415 | echo "The AWS CLI commands carrying out such actions can be found in: $helper_log" 1416 | echo 1417 | if [ -n "$dryrun" ]; then 1418 | echo "No actions were carried out in Dry-run mode" 1419 | else 1420 | echo "All done" 1421 | fi 1422 | fi 1423 | -------------------------------------------------------------------------------- /bin/connect_diff: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ############################################################ 4 | # 5 | # Find the differences of instance A and instance B and produce helper files. 6 | # Note: This script does not use AWS CLI but instead uses results from 7 | # connect_save on instance A and B. 8 | # 9 | 10 | VERSION=1.3.5 11 | SCRIPT_VERSION="$(basename $0) $VERSION" 12 | 13 | USAGE=$(cat <&2; exit 2; } 35 | version() { echo -e "$SCRIPT_VERSION"; exit; } 36 | dos2unix() { tr -d '\r'; } 37 | 38 | error() { echo -e "Error: $1" >&2; exit 1; } 39 | 40 | hex_cmd= 41 | if [ -x "$(command -v xxd)" ]; then 42 | hex_cmd="xxd -u -p -c1" 43 | elif [ -x "$(command -v hexdump)" ]; then 44 | hex_cmd="hexdump -v -e '/1 \"%02X\n\"'" 45 | elif [ -x "$(command -v od)" ]; then 46 | hex_cmd="od -An -vtx1 | tr [:lower:] [:upper:] | for i in \$(cat); do echo \$i; done" 47 | fi 48 | test -z "$hex_cmd" && error "Cannot find any hex conversion commands. Please install one of these: xxd, hexdump, od" 49 | hex_code() { printf '%s' "$1" | eval "$hex_cmd" | while read x; do printf "%%%s" "$x"; done } 50 | 51 | path_encode() { 52 | old_lc_collate=$LC_COLLATE 53 | LC_COLLATE=C 54 | local length="${#1}" 55 | for (( i = 0; i < length; i++ )); do 56 | local c="${1:$i:1}" 57 | case $c in 58 | [a-zA-Z0-9.~_-]) printf '%s' "$c";; 59 | *) x=$(hex_code "$c"); echo -n ${x//%0D/};; 60 | esac 61 | done 62 | LC_COLLATE=$old_lc_collate 63 | } 64 | 65 | add_file() { 66 | alias_dir="$1" 67 | file_suffix="$2" 68 | helper_file="$3" 69 | if [ ! -s "$alias_dir/$file_suffix" ]; then 70 | error "File missing or empty: $alias_dir/$file_suffix" 71 | exit 1 72 | else 73 | echo "$file_suffix" >> $helper_file 74 | fi 75 | } 76 | 77 | forced= 78 | ignore_improper_extended_ascii= 79 | lambda_prefix_a= 80 | lambda_prefix_b= 81 | lex_bot_prefix_a= 82 | lex_bot_prefix_b= 83 | while getopts "?fevl:b:" arg; do 84 | case "$arg" in 85 | f) forced=on;; 86 | e) ignore_improper_extended_ascii=on;; 87 | v) version;; 88 | l) lambda_prefix_a=${OPTARG%%=*} 89 | lambda_prefix_b=${OPTARG#*=} 90 | ;; 91 | b) lex_bot_prefix_a=${OPTARG%%=*} 92 | lex_bot_prefix_b=${OPTARG#*=} 93 | ;; 94 | *) usage;; 95 | esac 96 | done 97 | shift $((OPTIND-1)) 98 | 99 | if [ "$(hex_code "é")" != "%C3%A9" ]; then 100 | echo "WARNING: This system may not encode Extended ASCII characters properly." >&2 101 | if [ -n "$ignore_improper_extended_ascii" ]; then 102 | echo "Proceed regardless as the -e option is specified." >&2 103 | else 104 | cat <&2 105 | 106 | If your instance component names contain Extended ASCII characters, such as accented letters 107 | like é, this system will encode those names differently from standard encoding. 108 | 109 | If you are sure that your component names do not contain Extended ASCII characters, 110 | you may proceed regardless by running the command again with the -e option. 111 | EOD 112 | exit 1 113 | fi 114 | fi 115 | 116 | instance_alias_dir_a=$1 117 | instance_alias_a=$(basename "$instance_alias_dir_a") 118 | instance_alias_dir_b=$2 119 | instance_alias_b=$(basename "$instance_alias_dir_b") 120 | # helper=${3:-${instance_alias_a}_TO_${instance_alias_b}} 121 | helper=$3 122 | 123 | # Backward compatibility - if they are set, ignore the arguments 124 | lambda_prefix_a=${lambda_prefix_a:-$4} 125 | lambda_prefix_b=${lambda_prefix_b:-$5} 126 | 127 | if [ -z "$instance_alias_a" -o -z "$instance_alias_b" -o -z "$helper" ]; then 128 | usage 129 | fi 130 | 131 | if [ -d $helper ]; then 132 | if [ -n "$forced" ]; then 133 | echo "Existing Helper directory forcefully removed: $helper" 134 | rm -fr $helper 135 | else 136 | error "Helper directory already exists. Remove and try again (or use -f): $helper" 137 | fi 138 | fi 139 | 140 | # routing_profile_to_ignore="Basic Routing Profile" 141 | flow_template=$helper/flow_template.json 142 | module_template=$helper/module_template.json 143 | 144 | cat < $fn 168 | done 169 | 170 | 171 | ############################################################ 172 | # 173 | # Instance 174 | # 175 | 176 | echo "instance_alias_a=\"$instance_alias_a\"" | tee -a $helper_var 177 | echo "instance_alias_dir_a=\"$instance_alias_dir_a\"" | tee -a $helper_var 178 | echo "instance_alias_b=\"$instance_alias_b\"" | tee -a $helper_var 179 | echo "instance_alias_dir_b=\"$instance_alias_dir_b\"" | tee -a $helper_var 180 | 181 | . "$instance_alias_dir_a/instance.var" 182 | 183 | instance_id_a=$instance_Id 184 | instance_arn_a=$instance_Arn 185 | eval $(echo $instance_arn_a | (IFS=: read x x x r a x; echo "region_a=$r; aws_ac_a=$a")) 186 | region_a=$region_a 187 | aws_ac_a=$aws_ac_a 188 | echo "instance_id_a=\"$instance_id_a\"" | tee -a $helper_var 189 | echo "instance_arn_a=\"$instance_arn_a\"" | tee -a $helper_var 190 | echo "region_a=\"$region_a\"" | tee -a $helper_var 191 | echo "aws_ac_a=\"$aws_ac_a\"" | tee -a $helper_var 192 | echo "aws_profile_a=\"$aws_Profile\"" | tee -a $helper_var 193 | echo "lambda_prefix_a=\"$lambda_prefix_a\"" | tee -a $helper_var 194 | echo "lex_bot_prefix_a=\"$lex_bot_prefix_a\"" | tee -a $helper_var 195 | 196 | . "$instance_alias_dir_b/instance.var" 197 | instance_id_b=$instance_Id 198 | instance_arn_b=$instance_Arn 199 | eval $(echo $instance_arn_b | (IFS=: read x x x r a x; echo "region_b=$r; aws_ac_b=$a")) 200 | region_b=$region_b 201 | aws_ac_b=$aws_ac_b 202 | echo "instance_id_b=\"$instance_id_b\"" | tee -a $helper_var 203 | echo "instance_arn_b=\"$instance_arn_b\"" | tee -a $helper_var 204 | echo "region_b=\"$region_b\"" | tee -a $helper_var 205 | echo "aws_ac_b=\"$aws_ac_b\"" | tee -a $helper_var 206 | echo "aws_profile_b=\"$aws_Profile\"" | tee -a $helper_var 207 | echo "lambda_prefix_b=\"$lambda_prefix_b\"" | tee -a $helper_var 208 | echo "lex_bot_prefix_b=\"$lex_bot_prefix_b\"" | tee -a $helper_var 209 | 210 | # Use Instance B contact flow prefix 211 | echo "contact_flow_prefix=\"$instance_contact_flow_prefix\"" | tee -a $helper_var 212 | 213 | 214 | ############################################################ 215 | # 216 | # General SED commands 217 | # 218 | 219 | connect_arn_prefix_a="arn:aws:connect:$region_a:$aws_ac_a" 220 | connect_arn_prefix_b="arn:aws:connect:$region_b:$aws_ac_b" 221 | cat <> $helper_sed 222 | # General SED commands 223 | s%$instance_id_a%$instance_id_b%g 224 | s%$connect_arn_prefix_a%$connect_arn_prefix_b%g 225 | EOD 226 | 227 | lambda_arn_prefix_a="arn:aws:lambda:$region_a:$aws_ac_a:function:$lambda_prefix_a" 228 | lambda_arn_prefix_b="arn:aws:lambda:$region_b:$aws_ac_b:function:$lambda_prefix_b" 229 | if [ "$lambda_arn_prefix_a" != "$lambda_arn_prefix_b" ]; then 230 | echo "s%$lambda_arn_prefix_a%$lambda_arn_prefix_b%g" >> $helper_sed 231 | fi 232 | 233 | echo 234 | 235 | ############################################################ 236 | # 237 | # Prompts 238 | # 239 | 240 | echo Checking Prompts ... 241 | jq -r ".Id + \" \" + .Name" "$instance_alias_dir_a/prompts.json" | 242 | dos2unix | 243 | while read prompt_id_a prompt_name; do 244 | prompt_name_encoded=$(path_encode "$prompt_name") 245 | # prompt_id_b=$(jq -r "select(.Name == \"${prompt_name//\"/\\\"}\") | .Id" "$instance_alias_dir_b/prompts.json") 246 | prompt_id_b=$(sed -e's/\\"/%22/g' "$instance_alias_dir_b/prompts.json" | jq -r "select(.Name == \"${prompt_name//\"/%22}\") | .Id" | dos2unix) 247 | if [ -z "$prompt_id_b" ]; then 248 | echo "prompt_$prompt_name" >> $helper_new 249 | else 250 | echo "prompt_$prompt_name" >> $helper_old 251 | cat <> $helper_sed 252 | # Prompt: $prompt_name 253 | s%$prompt_id_a%$prompt_id_b%g 254 | EOD 255 | fi 256 | done 257 | test $? -eq 0 || error 258 | 259 | 260 | ############################################################ 261 | # 262 | # Hours of operations 263 | # 264 | 265 | echo Checking Hours of operations ... 266 | jq -r ".Id + \" \" + .Name" "$instance_alias_dir_a/hours.json" | 267 | dos2unix | 268 | while read hour_id_a hour_name; do 269 | hour_name_encoded=$(path_encode "$hour_name") 270 | # hour_id_b=$(jq -r "select(.Name == \"${hour_name//\"/\\\"}\") | .Id" "$instance_alias_dir_b/hours.json") 271 | hour_id_b=$(sed -e's/\\"/%22/g' "$instance_alias_dir_b/hours.json" | jq -r "select(.Name == \"${hour_name//\"/%22}\") | .Id" | dos2unix) 272 | if [ -z "$hour_id_b" ]; then 273 | add_file "$instance_alias_dir_a" "hour_$hour_name_encoded.json" $helper_new 274 | else 275 | add_file "$instance_alias_dir_b" "hour_$hour_name_encoded.json" $helper_old 276 | cat <> $helper_sed 277 | # Hour of operation: $hour_name 278 | s%$hour_id_a%$hour_id_b%g 279 | EOD 280 | fi 281 | done 282 | test $? -eq 0 || error 283 | 284 | 285 | ############################################################ 286 | # 287 | # Queues 288 | # 289 | 290 | echo Checking Queues ... 291 | jq -r ".Id + \" \" + .Name" "$instance_alias_dir_a/queues.json" | 292 | dos2unix | 293 | while read queue_id_a queue_name; do 294 | queue_name_encoded=$(path_encode "$queue_name") 295 | # queue_id_b=$(jq -r "select(.Name == \"${queue_name//\"/\\\"}\") | .Id" "$instance_alias_dir_b/queues.json") 296 | queue_id_b=$(sed -e's/\\"/%22/g' "$instance_alias_dir_b/queues.json" | jq -r "select(.Name == \"${queue_name//\"/%22}\") | .Id" | dos2unix) 297 | if [ -z "$queue_id_b" ]; then 298 | add_file "$instance_alias_dir_a" "queue_$queue_name_encoded.json" $helper_new 299 | else 300 | add_file "$instance_alias_dir_b" "queue_$queue_name_encoded.json" $helper_old 301 | cat <> $helper_sed 302 | # Queue: $queue_name 303 | s%$queue_id_a%$queue_id_b%g 304 | EOD 305 | fi 306 | done 307 | test $? -eq 0 || error 308 | 309 | 310 | ############################################################ 311 | # 312 | # Routing Profiles 313 | # 314 | 315 | echo Checking Routing Profiles ... 316 | jq -r ".Id + \" \" + .Name" "$instance_alias_dir_a/routings.json" | 317 | dos2unix | 318 | while read routing_id_a routing_name; do 319 | # if [ "$routing_name" == "$routing_profile_to_ignore" ]; then 320 | # continue 321 | # fi 322 | routing_name_encoded=$(path_encode "$routing_name") 323 | # routing_id_b=$(jq -r "select(.Name == \"${routing_name//\"/\\\"}\") | .Id" "$instance_alias_dir_b/routings.json") 324 | routing_id_b=$(sed -e's/\\"/%22/g' "$instance_alias_dir_b/routings.json" | jq -r "select(.Name == \"${routing_name//\"/%22}\") | .Id" | dos2unix) 325 | if [ -z "$routing_id_b" ]; then 326 | add_file "$instance_alias_dir_a" "routing_$routing_name_encoded.json" $helper_new 327 | add_file "$instance_alias_dir_a" "routingQs_$routing_name_encoded.json" $helper_new 328 | else 329 | add_file "$instance_alias_dir_b" "routing_$routing_name_encoded.json" $helper_old 330 | add_file "$instance_alias_dir_b" "routingQs_$routing_name_encoded.json" $helper_old 331 | cat <> $helper_sed 332 | # Routing Profile: $routing_name 333 | s%$routing_id_a%$routing_id_b%g 334 | EOD 335 | fi 336 | done 337 | test $? -eq 0 || error 338 | 339 | 340 | ############################################################ 341 | # 342 | # Contact Flow Modules 343 | # 344 | 345 | echo Checking Contact Flow Modules ... 346 | jq -r ".Id + \" \" + .Name" "$instance_alias_dir_a/modules.json" | 347 | dos2unix | 348 | while read module_id_a module_name; do 349 | module_name_encoded=$(path_encode "$module_name") 350 | # module_id_b=$(jq -r "select(.Name == \"${module_name//\"/\\\"}\") | .Id" "$instance_alias_dir_b/modules.json") 351 | module_id_b=$(sed -e's/\\"/%22/g' "$instance_alias_dir_b/modules.json" | jq -r "select(.Name == \"${module_name//\"/%22}\") | .Id" | dos2unix) 352 | if [ -z "$module_id_b" ]; then 353 | add_file "$instance_alias_dir_a" "module_$module_name_encoded.json" $helper_new 354 | else 355 | add_file "$instance_alias_dir_b" "module_$module_name_encoded.json" $helper_old 356 | cat <> $helper_sed 357 | # Contact Flow Module: $module_name 358 | s%$module_id_a%$module_id_b%g 359 | EOD 360 | fi 361 | done 362 | test $? -eq 0 || error 363 | 364 | 365 | ############################################################ 366 | # 367 | # Contact Flows 368 | # 369 | 370 | echo Checking Contact Flows ... 371 | jq -r ".Id + \" \" + .Name" "$instance_alias_dir_a/flows.json" | 372 | dos2unix | 373 | while read flow_id_a flow_name; do 374 | # flow_id_b=$(jq -r "select(.Name == \"${flow_name//\"/\\\"}\") | .Id" "$instance_alias_dir_b/flows.json") 375 | flow_id_b=$(sed -e's/\\"/%22/g' "$instance_alias_dir_b/flows.json" | jq -r "select(.Name == \"${flow_name//\"/%22}\") | .Id" | dos2unix) 376 | flow_name_encoded=$(path_encode "$flow_name") 377 | if [ -z "$flow_id_b" ]; then 378 | add_file "$instance_alias_dir_a" "flow_$flow_name_encoded.json" $helper_new 379 | else 380 | add_file "$instance_alias_dir_b" "flow_$flow_name_encoded.json" $helper_old 381 | cat <> $helper_sed 382 | # Contact Flow: $flow_name 383 | s%$flow_id_a%$flow_id_b%g 384 | EOD 385 | fi 386 | done 387 | test $? -eq 0 || error 388 | 389 | cat > $flow_template < $module_template <&2; exit 2; } 28 | version() { echo -e "$SCRIPT_VERSION"; exit; } 29 | dos2unix() { tr -d '\r'; } 30 | 31 | error() { 32 | if [ "$#" -ne 0 ]; then 33 | if [[ ! "$1" =~ ^[[:digit:]]+$ ]]; then 34 | # Not an AWS CLI error 35 | cat <&2 36 | Error: $* 37 | EOD 38 | else 39 | # An AWS CLI error 40 | line_no=$1 41 | cf=$2 42 | manifest=$3 43 | if [ -n "$cf" -a -n "$manifest" -a -n "$skip_cf_errors" ]; then 44 | # TEMPCF reused and needs to be emptied afterwards 45 | cat $manifest | 46 | jq -r "select(.Name != \"$cf\")" > $TEMPCF 47 | cat $TEMPCF > $manifest 48 | echo "\"$cf\" skipped and removed from $manifest." 49 | echo 50 | # Return ok to allow the main process to continue without this CF 51 | > $TEMPCF 52 | return 0 53 | else 54 | cat <&2 55 | Error at line ${line_no}. Recommended actions: 56 | 1. Create all required prompts on the target instance 57 | 2. Publish all in-scope contact flows and contact flow modules (or use -s to skip them) 58 | 3. Install the latest AWS CLI: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html 59 | EOD 60 | fi 61 | fi 62 | fi 63 | exit 1 64 | } 65 | 66 | hex_cmd= 67 | if [ -x "$(command -v xxd)" ]; then 68 | hex_cmd="xxd -u -p -c1" 69 | elif [ -x "$(command -v hexdump)" ]; then 70 | hex_cmd="hexdump -v -e '/1 \"%02X\n\"'" 71 | elif [ -x "$(command -v od)" ]; then 72 | hex_cmd="od -An -vtx1 | tr [:lower:] [:upper:] | for i in \$(cat); do echo \$i; done" 73 | fi 74 | test -z "$hex_cmd" && error "Cannot find any hex conversion commands. Please install one of these: xxd, hexdump, od" 75 | hex_code() { printf '%s' "$1" | eval "$hex_cmd" | while read x; do printf "%%%s" "$x"; done } 76 | 77 | path_encode() { 78 | old_lc_collate=$LC_COLLATE 79 | LC_COLLATE=C 80 | local length="${#1}" 81 | for (( i = 0; i < length; i++ )); do 82 | local c="${1:$i:1}" 83 | case $c in 84 | [a-zA-Z0-9.~_-]) printf '%s' "$c";; 85 | *) x=$(hex_code "$c"); echo -n ${x//%0D/};; 86 | esac 87 | done 88 | LC_COLLATE=$old_lc_collate 89 | } 90 | 91 | # AWS CLI needs to output JSON 92 | export AWS_DEFAULT_OUTPUT=json 93 | aws_cli_encoding=UTF-8 94 | 95 | aws_cli_out_filter() { 96 | if [ -n "$codepage" -a "$codepage" != "$aws_cli_encoding" ]; then 97 | cat > $TEMPOF 98 | iconv -f $codepage -t $aws_cli_encoding $TEMPOF 2> /dev/null 99 | test $? -eq 0 || cat $TEMPOF 100 | else 101 | cat 102 | fi 103 | } 104 | 105 | add_json_attribute() { 106 | attr_name=$1 107 | attr_value=$2 108 | key_id=$3 109 | manifest=$4 110 | #echo "Adding $attr_name=\"$attr_value\" to $key_id in $manifest" 111 | # TEMPCF reused and needs to be emptied afterwards 112 | cat $manifest | 113 | jq -rs "map(if .Id == \"$key_id\" then . + {$attr_name: \"${attr_value//\"/\\\"}\"} else . end) | .[]" > $TEMPCF 114 | cat $TEMPCF > $manifest 115 | > $TEMPCF 116 | # jq -r "map(.Id |= if .Id == \"$key_id\" then . else . end)" 117 | } 118 | 119 | aws_connect() { 120 | local cmd="" 121 | local ii 122 | for ii; do 123 | [[ "$ii" == *" "* ]] && cmd="$cmd \"$ii\"" || cmd="$cmd $ii" 124 | done 125 | echo "aws connect$profile_flag$cmd" >> "$aws_cli_log" 126 | eval "aws connect$profile_flag$cmd | aws_cli_out_filter" 2> $TEMPERR 127 | local ret=$? 128 | if [ -s $TEMPERR ]; then 129 | cat $TEMPERR | tee -a "$aws_cli_log" >&2 130 | fi 131 | return $ret 132 | } 133 | 134 | TEMPFILE=$(mktemp) 135 | TEMPFILE2=${TEMPFILE}_2 136 | TEMPCF=${TEMPFILE}_cf 137 | TEMPOF=${TEMPFILE}_of 138 | TEMPERR=${TEMPFILE}_err 139 | trap 'rm -r -- $TEMPFILE $TEMPFILE2 $TEMPCF $TEMPERR' EXIT 140 | touch $TEMPFILE $TEMPFILE2 $TEMPCF $TEMPOF $TEMPERR 141 | 142 | maxitems=999 143 | 144 | forced= 145 | skip_cf_errors= 146 | ignore_improper_extended_ascii= 147 | ignore_prefix= 148 | jq_prefix_filter= 149 | jq_prefix_filter_text= 150 | # codepage=${LANG##*.} 151 | eval $(locale -k LC_CTYPE | grep charmap) 152 | codepage=${charmap:-$aws_cli_encoding} 153 | codepage=${codepage##*.} 154 | while getopts "?fsevp:c:G:C:" arg; do 155 | case "$arg" in 156 | f) forced=on;; 157 | s) skip_cf_errors=on;; 158 | e) ignore_improper_extended_ascii=on;; 159 | v) version;; 160 | p) profile=$OPTARG;; 161 | c) contact_flow_prefix=$OPTARG;; 162 | G) ignore_prefix=$OPTARG 163 | jq_prefix_filter=" | select(.Name | test(\"^($ignore_prefix)\") | not)" 164 | jq_prefix_filter_text=" excluding those with names prefixed with \"$ignore_prefix\"" 165 | ;; 166 | C) codepage=$OPTARG;; 167 | *) usage;; 168 | esac 169 | done 170 | shift $((OPTIND-1)) 171 | 172 | if [ "$(hex_code "é")" != "%C3%A9" ]; then 173 | echo "WARNING: This system may not encode Extended ASCII characters properly." >&2 174 | if [ -n "$ignore_improper_extended_ascii" ]; then 175 | echo "Proceed regardless as the -e option is specified." >&2 176 | else 177 | cat <&2 178 | 179 | If your instance component names contain Extended ASCII characters, such as accented letters 180 | like é, this system will encode those names differently from standard encoding. 181 | 182 | If you are sure that your component names do not contain Extended ASCII characters, 183 | you may proceed regardless by running the command again with the -e option. 184 | EOD 185 | exit 1 186 | fi 187 | fi 188 | 189 | instance_alias_dir=$1 190 | instance_alias=$(basename "$instance_alias_dir") 191 | 192 | # Backward compatibility - if it is set, ignore the argument 193 | profile=${profile:-$2} 194 | profile_flag=${profile:+ --profile $profile} 195 | 196 | # Backward compatibility - if it is set, ignore the argument 197 | contact_flow_prefix=${contact_flow_prefix:-$3} 198 | contact_flow_prefix_filter=${contact_flow_prefix:+" | select(.Name | test(\"^($contact_flow_prefix|Default ).*\"))"} 199 | contact_flow_prefix_text="${contact_flow_prefix:+ with names prefixed with \"$contact_flow_prefix\" or Default}" 200 | 201 | if [ -z "$instance_alias_dir" ]; then 202 | usage 203 | fi 204 | 205 | if [ -d "$instance_alias_dir" ]; then 206 | if [ -n "$forced" ]; then 207 | echo "Existing Alias directory forcefully removed: \"$instance_alias_dir\"" 208 | rm -fr "$instance_alias_dir" 209 | else 210 | error "Alias directory already exists. Remove and try again (or use -f): \"$instance_alias_dir\"" 211 | fi 212 | fi 213 | 214 | cat < "$aws_cli_log" 236 | 237 | # First aws cli; only mkdir if successful. 238 | aws_connect list-instances --max-items $maxitems > $TEMPFILE || error $LINENO 239 | 240 | instance_id=$(cat $TEMPFILE | 241 | jq -r ".InstanceSummaryList[] | select(.InstanceAlias == \"$instance_alias\")" | 242 | tee "$instance_alias_dir/instance.json" | 243 | jq -r ".Id" | 244 | dos2unix) 245 | echo "Instance ID: $instance_id" 246 | 247 | test -n "$instance_id" || error "Instance $instance_alias does not exist" 248 | 249 | cat "$instance_alias_dir/instance.json" | 250 | grep : | 251 | sed -e's/ *"/instance_/' -e's/": /=/' -e's/,$//' > "$instance_alias_dir/instance.var" 252 | echo "aws_Profile=\"$profile\"" >> "$instance_alias_dir/instance.var" 253 | echo "instance_contact_flow_prefix=\"$contact_flow_prefix\"" >> "$instance_alias_dir/instance.var" 254 | 255 | 256 | ############################################################ 257 | # 258 | # Prompts 259 | # 260 | 261 | aws_connect list-prompts \ 262 | --instance-id $instance_id \ 263 | --max-items $maxitems \ 264 | > $TEMPFILE || error $LINENO 265 | 266 | cat $TEMPFILE | 267 | jq -r ".PromptSummaryList |= sort_by(.Name) | .[][]" | 268 | tee "$instance_alias_dir/prompts.json" | 269 | echo -e "\n$(jq -s "length") prompts listed in \"$instance_alias_dir/prompts.json\"" 270 | 271 | 272 | ############################################################ 273 | # 274 | # Hours of operations 275 | # 276 | 277 | aws_connect list-hours-of-operations \ 278 | --instance-id $instance_id \ 279 | --max-items $maxitems \ 280 | > $TEMPFILE || error $LINENO 281 | 282 | cat $TEMPFILE | 283 | # jq -r ".HoursOfOperationSummaryList |= sort_by(.Name) | .[][]" | 284 | jq -r ".HoursOfOperationSummaryList[]$jq_prefix_filter" | 285 | jq -s "sort_by(.Name) | .[]" | 286 | tee "$instance_alias_dir/hours.json" | 287 | echo -e "\n$(jq -s "length") hours of operations listed in \"$instance_alias_dir/hours.json\"$jq_prefix_filter_text" 288 | 289 | jq -r ".Id + \" \" + .Name" "$instance_alias_dir/hours.json" | 290 | dos2unix | 291 | while read hour_id hour_name; do 292 | echo "Exporting hours of operation $hour_name" 293 | hour_name_encoded=$(path_encode "$hour_name") 294 | aws_connect describe-hours-of-operation \ 295 | --instance-id $instance_id \ 296 | --hours-of-operation-id $hour_id \ 297 | > "$instance_alias_dir/hour_$hour_name_encoded.json" || error $LINENO 298 | done 299 | test $? -eq 0 || error 300 | 301 | 302 | ############################################################ 303 | # 304 | # Queues 305 | # 306 | 307 | aws_connect list-queues \ 308 | --instance-id $instance_id \ 309 | --max-items $maxitems \ 310 | --queue-types "STANDARD" \ 311 | > $TEMPFILE || error $LINENO 312 | 313 | cat $TEMPFILE | 314 | jq -r ".QueueSummaryList[] | select(.QueueType != \"AGENT\")$jq_prefix_filter" | 315 | jq -s "sort_by(.Name) | .[]" | 316 | tee "$instance_alias_dir/queues.json" | 317 | echo -e "\n$(jq -s "length") queues listed in \"$instance_alias_dir/queues.json\"$jq_prefix_filter_text" 318 | 319 | jq -r ".Id + \" \" + .Name" "$instance_alias_dir/queues.json" | 320 | dos2unix | 321 | while read queue_id queue_name; do 322 | echo "Exporting queue $queue_name" 323 | queue_name_encoded=$(path_encode "$queue_name") 324 | aws_connect describe-queue \ 325 | --instance-id $instance_id \ 326 | --queue-id $queue_id \ 327 | > "$instance_alias_dir/queue_$queue_name_encoded.json" || error $LINENO 328 | done 329 | test $? -eq 0 || error 330 | 331 | 332 | ############################################################ 333 | # 334 | # Routing Profiles 335 | # 336 | 337 | aws_connect list-routing-profiles \ 338 | --instance-id $instance_id \ 339 | --max-items $maxitems \ 340 | > $TEMPFILE || error $LINENO 341 | 342 | cat $TEMPFILE | 343 | jq -r ".RoutingProfileSummaryList[]$jq_prefix_filter" | 344 | jq -s "sort_by(.Name) | .[]" | 345 | tee "$instance_alias_dir/routings.json" | 346 | echo -e "\n$(jq -s "length") routing profiles listed in \"$instance_alias_dir/routings.json\"$jq_prefix_filter_text" 347 | 348 | jq -r ".Id + \" \" + .Name" "$instance_alias_dir/routings.json" | 349 | dos2unix | 350 | while read routing_id routing_name; do 351 | echo "Exporting routing profile $routing_name" 352 | routing_name_encoded=$(path_encode "$routing_name") 353 | aws_connect describe-routing-profile \ 354 | --instance-id $instance_id \ 355 | --routing-profile-id $routing_id | 356 | jq -r "del(.RoutingProfile.NumberOfAssociatedQueues, .RoutingProfile.NumberOfAssociatedUsers)" \ 357 | > "$instance_alias_dir/routing_$routing_name_encoded.json" || error $LINENO 358 | aws_connect list-routing-profile-queues \ 359 | --instance-id $instance_id \ 360 | --routing-profile-id $routing_id \ 361 | --max-items $maxitems \ 362 | > "$instance_alias_dir/routingQs_$routing_name_encoded.json" || error $LINENO 363 | done 364 | test $? -eq 0 || error 365 | 366 | 367 | ############################################################ 368 | # 369 | # Contact Flow Modules 370 | # 371 | 372 | aws_connect list-contact-flow-modules \ 373 | --instance-id $instance_id \ 374 | --max-items $maxitems \ 375 | > $TEMPFILE || error $LINENO 376 | 377 | cat $TEMPFILE | 378 | jq -r ".ContactFlowModulesSummaryList[]${contact_flow_prefix_filter}${jq_prefix_filter}" | 379 | jq -s "sort_by(.Name) | .[]" | 380 | tee "$instance_alias_dir/modules.json" | 381 | echo -e "\n$(jq -s "length") contact flow modules listed in \"$instance_alias_dir/modules.json\"$contact_flow_prefix_text$jq_prefix_filter_text" 382 | 383 | 384 | ############################################################ 385 | # 386 | # Export Contact Flow Modules 387 | # 388 | 389 | cat "$instance_alias_dir/modules.json" > $TEMPFILE 390 | jq -r ".Id + \" \" + .Name" $TEMPFILE | 391 | dos2unix | 392 | while read module_id module_name; do 393 | echo "Exporting contact flow module $module_name" 394 | module_name_encoded=$(path_encode "$module_name") 395 | aws_connect describe-contact-flow-module \ 396 | --instance-id $instance_id \ 397 | --contact-flow-module-id $module_id > $TEMPCF \ 398 | || error $LINENO "$module_name" "$instance_alias_dir/modules.json" 399 | if [ -s $TEMPCF ]; then 400 | # Status == "saved" is considered a failure, 401 | # not to copy an unpublished contact flow module 402 | cfm_status=$(jq -r ".ContactFlowModule.Status" $TEMPCF | dos2unix) 403 | if [ "$cfm_status" == "published" ]; then 404 | cat $TEMPCF | 405 | jq -r '.ContactFlowModule.Content' > "$instance_alias_dir/module_$module_name_encoded.json" 406 | cfm_description=$(cat $TEMPCF | jq -r '.ContactFlowModule.Description') 407 | add_json_attribute Description "$cfm_description" $module_id "$instance_alias_dir/modules.json" 408 | else 409 | echo 410 | echo "$module_name: Contact flow module not published" 411 | # Let error decide if continue or not 412 | error $LINENO "$module_name" "$instance_alias_dir/modules.json" 413 | fi 414 | fi 415 | done 416 | test $? -eq 0 || error 417 | 418 | 419 | ############################################################ 420 | # 421 | # Contact Flows 422 | # 423 | 424 | aws_connect list-contact-flows \ 425 | --instance-id $instance_id \ 426 | --max-items $maxitems \ 427 | > $TEMPFILE || error $LINENO 428 | 429 | cat $TEMPFILE | 430 | jq -r ".ContactFlowSummaryList[]${contact_flow_prefix_filter}${jq_prefix_filter}" | 431 | jq -s "sort_by(.Name) | .[]" | 432 | tee "$instance_alias_dir/flows.json" | 433 | echo -e "\n$(jq -s "length") contact flows listed in \"$instance_alias_dir/flows.json\"$contact_flow_prefix_text$jq_prefix_filter_text" 434 | 435 | 436 | ############################################################ 437 | # 438 | # Export Contact Flows 439 | # 440 | 441 | cat "$instance_alias_dir/flows.json" > $TEMPFILE 442 | jq -r ".Id + \" \" + .Name" $TEMPFILE | 443 | dos2unix | 444 | while read flow_id flow_name; do 445 | echo "Exporting contact flow $flow_name" 446 | aws_connect describe-contact-flow \ 447 | --instance-id $instance_id \ 448 | --contact-flow-id $flow_id > $TEMPCF \ 449 | 2> /dev/null 450 | # || error $LINENO "$flow_name" "$instance_alias_dir/flows.json" 451 | # AWS CLI seems not returning error for unpublished 452 | # Need to check for an empty $TEMPCF 453 | flow_name_encoded=$(path_encode "$flow_name") 454 | if [ -s $TEMPCF ]; then 455 | cat $TEMPCF | 456 | jq -r '.ContactFlow.Content' > "$instance_alias_dir/flow_$flow_name_encoded.json" 457 | cf_description=$(cat $TEMPCF | jq -r '.ContactFlow.Description') 458 | add_json_attribute Description "$cf_description" $flow_id "$instance_alias_dir/flows.json" 459 | else 460 | echo 461 | echo "$flow_name: Contact flow not published" 462 | error $LINENO "$flow_name" "$instance_alias_dir/flows.json" 463 | fi 464 | done 465 | test $? -eq 0 || error 466 | 467 | echo 468 | echo "All done" 469 | -------------------------------------------------------------------------------- /examples/codebuild/buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | # Repository directory structure 4 | # . 5 | # ├── bin 6 | # │ ├── connect_copy 7 | # │ ├── connect_diff 8 | # │ └── connect_save 9 | # ├── buildspec.yml 10 | # └── src 11 | # └── source_dir_saved_by_connect_save 12 | 13 | # Artifacts directory structure 14 | # artifacts 15 | # ├── post-build 16 | # │ ├── target_instance_alias 17 | # │ └── target_instance_alias.log 18 | # ├── pre-build 19 | # │ ├── target_instance_alias 20 | # │ └── target_instance_alias.log 21 | # └── helper 22 | # ├── ... 23 | # └── ... 24 | 25 | env: 26 | variables: 27 | LANG: "en_GB.UTF-8" 28 | source_dir: "src/source_dir_saved_by_connect_save" 29 | target_dir: "artifacts/pre-build/target_instance_alias" 30 | target_post_build_dir: "artifacts/post-build/target_instance_alias" 31 | helper: "artifacts/helper" 32 | source_lambda_prefix: "source-lambda-prefix-" 33 | target_lambda_prefix: "target-lambda-prefix-" 34 | # contact_flow_prefix: "in_scope_contact_flow_prefix" 35 | # save_options: "-G component_prefix_to_ignore" 36 | phases: 37 | install: 38 | commands: 39 | - jq --version 40 | - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" 41 | - unzip awscliv2.zip &> /dev/null 42 | - ./aws/install -b /usr/local/bin/sbt/bin 43 | pre_build: 44 | commands: 45 | - bin/connect_save $save_options "$target_dir" "" "$contact_flow_prefix" 46 | - bin/connect_diff "$source_dir" "$target_dir" "$helper" "$source_lambda_prefix" "$target_lambda_prefix" 47 | build: 48 | commands: 49 | - bin/connect_copy -d "$helper" 50 | - test $? -ne 0 && echo "No updates" || bin/connect_copy "$helper" 51 | post_build: 52 | commands: 53 | - bin/connect_save $save_options "$target_post_build_dir" "" "$contact_flow_prefix" 54 | artifacts: 55 | files: 56 | - "**/*" 57 | base-directory: artifacts 58 | --------------------------------------------------------------------------------