diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..d7285d810e --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +docs export-ignore \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..16e3307388 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,3 @@ +**Features:** + +**Fixes:** diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000000..1f37c668c9 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,25 @@ +name: Build Go + +on: + push: + branches: + - "main" + pull_request: + branches: + - "main" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up go + uses: actions/setup-go@v3 + with: + go-version: 1.20.14 + + - name: Build + run: go build ./... + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..7dcba83bfe --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,30 @@ +name: Release + +on: + push: + tags: + - '*' + +jobs: + release: + if: contains(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up go + uses: actions/setup-go@v3 + with: + go-version: 1.19.2 + + - name: Build + run: bash hack/github-release.sh + + - name: Create release + uses: softprops/action-gh-release@v1 + with: + token: ${{ secrets.RELEASE_GITHUB_TOKEN }} + files: | + out/*.zip + diff --git a/.gitignore b/.gitignore index 1fcb1529f8..488515802a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ out +.vscode/ +.idea/ +bin/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 3979ba3f6b..9fc72fd0df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,248 @@ -## Change Log +## 0.3.0 (2024-09-20) -v0.1.3 -* integrate auto completion. +* support naming in batch on creating uhost + +## 0.2.0 (2024-06-12) + +* ssh key pair and security group support for creating uhost + +## 0.1.49 (2024-05-09) + +* fix cli version config + +## 0.1.48 (2024-05-09) + +* update golang version to 1.19 + +## 0.1.47 (2024-05-08) + +* support repeating generic api call + +## 0.1.46 (2023-12-28) + +* update ucloud go sdk version + +## 0.1.45 (2023-12-18) + +* fix reinstall password encoding + +## 0.1.44 (2023-08-15) + +* fix failing to specify release-eip and delete-cloud-disk to false + +## 0.1.43 (2023-07-13) + +* skip no permission region when query all uhosts in all regions + +## 0.1.42 (2023-02-22) + +* add concurrent parameter for uhost creating to tune concurrent level + +## 0.1.41 (2022-11-30) + +* remove bandwith limit for unet and udpn product in cli + +## 0.1.40 (2022-08-31) + +* add `signature` command to calculate signature quickly. + +## 0.1.39 (2022-06-13) + +* fix init failure when user donot have a default project in ucloud console. +* update the description of init. + +## 0.1.38 (2022-04-27) + +* fix build failure when using go 1.18 on darwin_arm64. ([golang/go#49219](https://github.com/golang/go/issues/49219)) + +## 0.1.36 (2021-07-07) + +ENHANCEMENTS: + +* add the flags: `--instance-type`, `--forward-region`, `--bandwidth-package` about command `gssh create` to customize specific instance type of global ssh. +* add response fields: `GlobalSSHPort`, `InstanceType` about command `gssh list` to list specific instance type of global ssh. +* update cmd `uhost create` about bind EIP: EIP creates and binds to UHost when UHost creating instead of after UHost creating. + +## 0.1.35 (2020-11-11) + +ENHANCEMENTS: + +* add the flag `--user-data-base64` about command `ucloud uhost create` to customize the startup behaviors when launching the uhost instance and the value must be base64-encode.(#55) + +## 0.1.34 (2020-11-11) + +ENHANCEMENTS: + +* add the flag `--user-data` about command `ucloud uhost create` to customize the startup behaviors when launching the uhost instance.(#54) +* add the flag `--gpu-type` about command `ucloud uhost create` to define the type of GPU instance.(#54) + +## 0.1.33 + +* Add command 'ucloud api', which can call any API of ucloud like this + - ucloud api --Action DescribeUHostInstance --Region cn-bj2 or + - ucloud api --local-file ./create_uhost.json +* Adapt to cloudshell + +## 0.1.32 + +* Fixbug for creating uhost with shared bandwith. Now you can create uhost bound with shared bandwith using follow command. +``` +ucloud uhost create --cpu 1 --memory-gb 2 --image-id uimage-xxx --password xxxxx --create-eip-traffic-mode ShareBandwidth --shared-bw-id bwshare-lxxxx +``` + +## 0.1.31 + +* fixbug, password missed when creating redis + +## 0.1.30 + +* support creating uhost without data disk. +* default value of flag '--machine-type' changed to 'N' from empty when creating uhost. + +## 0.1.29 + +* resize attached disk without stop uhost +* make batch creating uhost faster + +## 0.1.28 + +* command 'ucloud uhost resize' add flag '--data-disk-id', to resize the specified udisk. +* fixbug #45 + +## 0.1.27 + +* Enable hot-plug for uhost when running 'ucloud uhost create' +* Add command 'ucloud uhost leave-isolation-group', 'ucloud uhost isolation-group create' and 'ucloud uhost isolation-group delete' + +## 0.1.26 + +* fixbug about base-url + +## 0.1.25 + +* ask permission for upload log when executing 'ucloud init' + +## 0.1.24 + +* add global flags --base-url, --timeout-sec, --max-retry-times +* command [ucloud uhost create] add flag --hot-plug, --isolation-group +* add command [ucloud uhost isolation-group list] + +## 0.1.23 + +* fix dead lock when creating uhosts in parallel +* refactor part of eip and ulb operations + +## 0.1.22 + +* Add global flag '--public-key' and '--private-key' to override public-key and private-key in local config files. +* Add flag '--max-retry-times' for command 'ucloud config' so that users can set retry times for failed idempotent API calls. +* Add flag '--region-all' and '--output' for command 'ucloud uhost list' so that users can list uhosts in all regions and display more infomations about uhost. + +## 0.1.21 + +* Add global flag '--profile' to specify profile for any command. +* Add command 'ucloud ext uhost switch-eip' + +## 0.1.20 + +* Add command: + ucloud pathx uga create | delete | list | describe | add-port | delete-port + ucloud pathx upath list + +## 0.1.19 + +* Bugfix for running command ucloud init failed. + +## 0.1.18 + +* Add following commands: + - `ucloud config add` + - `ucloud config update` + - `ucloud redis restart` + - `ucloud memcache restart` + +* Command [ucloud uhost list --uhost-id-only] list uhost-ids separated by comma +* Command [ucloud uhost delete --uhost-id xx,xx] can delete uhost instances concurrently. + You can use [ucloud uhost delete --uhost-id \`ucloud uhost list --uhost-id-only --page-off\`] to delete all uhost instances in parallel. + +## 0.1.17 + +* add flags page-off and uhost-id-only for uhost list + +## 0.1.16 + +* Support log rotation. Log file path $HOME/.ucloud/cli.log. +* Bugfix for display nothing when uhost create failed + +## 0.1.15 + +* Update documents +* Add test for uhost + +## 0.1.14 + +* Create uhost concurrently + +## 0.1.13 + +* Update version of ucloud-sdk-go to fix bug + +## 0.1.12 + +* Preliminary support umem + +## 0.1.11 + +* Use go modules to manage dependencies +* Fix bug for uhost clone + +## 0.1.10 + +* Support udb mysql + +## 0.1.9 + +* Better flag value completion with local cache and multiple resource ID completion +* Command structure adjustment + - ucloud bw-pkg => ucloud bw pkg + - ucloud shared-bw => ucloud bw shared + - ucloud ulb-vserver => ucloud ulb vserver + - ucloud ulb-ssl-certificate => ucloud ulb ssl + - ucloud ulb-vserver add-node/update-node/delete-node/list-node => ucloud ulb vserver backend add/update/delete/list + - ucloud ulb-vserver add-policy/list-policy/update-policy/delete-policy => ucloud ulb vserver policy add/list/update/delete + +## 0.1.8 + +* Support ulb + +## 0.1.7 + +* Add udpn, firewall, shared bandwidth and bandwidth package; Refactor vpc, subnet and eip + +## 0.1.6 + +* Improve uhost,image and disk-snapshot + +## 0.1.5 + +* support batch operation. + +## 0.1.4 + +* Support udisk. +* Polling udisk and uhost long time operation +* Async complete resource-id + +## 0.1.3 + +* Integrate auto completion. * Support uhost create, stop, delete and so on. -v0.1.2 -* simplify config and completion. +## 0.1.2 + +* Simplify config and completion. + +## 0.1.1 -v0.1.1 -* UHost list; EIP list,delete and allocate; GlobalSSH list,delete,modify and create. \ No newline at end of file +* UHost list; EIP list,delete and allocate; GlobalSSH list,delete,modify and create. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..5bcfd72b58 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM golang:latest + +WORKDIR /root + +RUN apt update \ + && apt install zsh vim -y \ + && wget https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh \ + && sh install.sh + +RUN git clone https://github.com/ucloud/ucloud-cli.git \ + && cd ucloud-cli && make install && cd ../ \ + && echo "autoload -U +X bashcompinit && bashcompinit \ncomplete -F $(which ucloud) ucloud" >> ~/.zshrc diff --git a/Makefile b/Makefile index 663ceb6e6d..d9d35d62ba 100644 --- a/Makefile +++ b/Makefile @@ -1,31 +1,40 @@ -export VERSION=0.1.5 +export VERSION=0.3.0 -.PHONY : build -build: - go install && mv ../../../../bin/ucloud-cli /usr/local/bin/ucloud +GOFMT_FILES?=$$(find . -name '*.go' |grep -v vendor) -.PHONY : build_mac -build_mac: - GOOS=darwin GOARCH=amd64 go build -o out/ucloud main.go - tar zcvf out/ucloud-cli-macosx-${VERSION}-amd64.tgz -C out ucloud - shasum -a 256 out/ucloud-cli-macosx-${VERSION}-amd64.tgz +.PHONY : install +install: + go build -v -mod=vendor -o out/ucloud main.go + cp out/ucloud /usr/local/bin -.PHONY : build_linux -build_linux: - GOOS=linux GOARCH=amd64 go build -o out/ucloud main.go - tar zcvf out/ucloud-cli-linux-${VERSION}-amd64.tgz -C out ucloud - shasum -a 256 out/ucloud-cli-linux-${VERSION}-amd64.tgz +.PHONY : build-darwin-amd64 +build-darwin-amd64: + GOOS=darwin GOARCH=amd64 go build -mod=vendor -o out/darwin_amd64/ucloud main.go + @cp LICENSE out/darwin_amd64 -.PHONY : build_windows -build_windows: - GOOS=windows GOARCH=amd64 go build -o out/ucloud.exe main.go - zip -r out/ucloud-cli-windows-${VERSION}-amd64.zip out/ucloud.exe - shasum -a 256 out/ucloud-cli-windows-${VERSION}-amd64.zip +.PHONY : build-darwin-arm64 +build-darwin-arm64: + GOOS=darwin GOARCH=arm64 go build -mod=vendor -o out/darwin_arm64/ucloud main.go + @cp LICENSE out/darwin_arm64 -.PHONY : build_all -build_all: build_mac build_linux build_windows +.PHONY : build-linux-amd64 +build-linux-amd64: + GOOS=linux GOARCH=amd64 go build -mod=vendor -o out/linux_amd64/ucloud main.go + @cp LICENSE out/linux_amd64 -.PHONY : install -install: - go build -o out/ucloud main.go - cp out/ucloud /usr/local/bin +.PHONY : build-linux-arm64 +build-linux-arm64: + GOOS=linux GOARCH=amd64 go build -mod=vendor -o out/linux_arm64/ucloud main.go + @cp LICENSE out/linux_arm64 + +.PHONY : build-windows-amd64 +build-windows-amd64: + GOOS=windows GOARCH=amd64 go build -mod=vendor -o out/windows_amd64/ucloud.exe main.go + @cp LICENSE out/windows_amd64 + +.PHONY : build-all +build-all: build-darwin-amd64 build-darwin-arm64 build-linux-arm64 build-linux-amd64 build-windows-amd64 + +.PHONY: fmt +fmt: + gofmt -w -s $(GOFMT_FILES) diff --git a/README-CN.md b/README-CN.md new file mode 100644 index 0000000000..0922dd51a8 --- /dev/null +++ b/README-CN.md @@ -0,0 +1,254 @@ +[English](./README.md) | 简体中文 +## UCloud CLI + +![](./docs/_static/ucloud_cli_demo.gif) + +UCloud CLI为管理UCloud平台上的资源和服务提供了一致性的操作接口,它使用[ucloud-sdk-go](https://github.com/ucloud/ucloud-sdk-go)调用[UCloud OpenAPI](https://docs.ucloud.cn/api/summary/overview),从而实现对资源和服务的操作,兼容Linux, macOS和Windows平台 https://docs.ucloud.cn/developer/cli/index + +## 在macOS或Linux平台安装UCloud-CLI + +**通过Homebrew安装(在macOS平台上推荐此方式)** + +[Homebrew](https://docs.brew.sh/Installation) 是macOS平台上非常流行的包管理工具,您可以通过如下命令轻松安装或升级UCloud-CLI + +安装UCloud-CLI +``` +brew install ucloud +``` + +升级到最新版本 + +``` +brew upgrade ucloud +``` + +如果安装过程中遇到错误,请先执行如下命令更新Homebrew + +``` +brew update +``` + +如果问题依然存在,执行如下命令获取更多帮助 + +``` +brew doctor +``` + +**基于源代码编译(需要本地安装golang)** + +如果您已经安装了git和golang在您的平台上,您可以使用如下命令下载源代码并编译 + +``` +git clone https://github.com/ucloud/ucloud-cli.git +cd ucloud-cli +make install +``` + +升级到最新版本 +``` +cd /path/to/ucloud-cli +git pull +make install +``` + +**下载已编译好的二进制可执行文件(Linux上如果选不到非常方便的安装方式,推荐用此办法安装)** + +打开ucloud-cli的[发布页面](https://github.com/ucloud/ucloud-cli/releases),找到适合您平台的ucloud-cli压缩包。点击链接进行下载,下载后,通过比对sha256摘要来检验下载文件未被劫持,然后把ucloud-cli可执行文件解压到$PATH环境变量包含的目录,操作命令如下: + +举个例子 +``` +curl -OL https://github.com/ucloud/ucloud-cli/releases/download/0.1.23/ucloud-cli-linux-0.1.23-amd64.tgz +echo "b480f8621e8d0bd2c121221857029320eb49be708f4d7cb1b197cdc58b071c09 *ucloud-cli-linux-0.1.23-amd64.tgz" | shasum -c //检查下载的tar包是否被劫持,从发布页面获取sha256摘要 +tar zxf ucloud-cli-linux-0.1.23-amd64.tgz -C /usr/local/bin/ +``` + +## 在Windows平台上安装UCloud-CLI + +**基于源代码编译** + +从UCloud-CLI的[发布页面](https://github.com/ucloud/ucloud-cli/releases)下载源代码并解压,您也可以通过git下载源代码,打开Git Bash, 执行命令```git clone https://github.com/ucloud/ucloud-cli.git```。 +切换到源代码所在的目录,编译源代码(执行命令 ```go build -mod=vendor -o ucloud.exe```),然后把可执行文件ucloud.exe所在目录添加到PATH环境变量中,具体操作可参看[文档](https://www.java.com/en/download/help/path.xml) +配置完成后,打开终端(cmd或power shell),执行命令```ucloud --version```检查是否安装成功。 + + +**下载二进制可执行文件** + +打开ucloud-cli的[发布页面](https://github.com/ucloud/ucloud-cli/releases),找到适合您平台的ucloud-cli压缩包。点击链接进行下载并解压,然后把可执行文件ucloud.exe所在目录添加到PATH环境变量中,添加环境变量的操作可参考[文档](https://www.java.com/en/download/help/path.xml) + +## 在Docker容器中使用UCloud-CLI +如果您已安装Docker, 通过如下命令拉取已打包UCloud-CLI的镜像。镜像打包[Dockerfile](./Dockerfile) +``` +docker pull uhub.service.ucloud.cn/ucloudcli/ucloud-cli:source-code +``` + +基于此镜像创建容器 +``` +docker run --name ucloud-cli -it -d uhub.service.ucloud.cn/ucloudcli/ucloud-cli:source-code +``` +连接到容器,开始使用UCloud-CLI +``` +docker exec -it ucloud-cli zsh +``` + +## 开启命令补全(bash或zsh shell) + +UCloud-CLI支持命令自动补全,开启后,您只需要输入命令的部分字符,然后敲击Tab键即可自动补全命令的其余字符。 + +**Bash shell** + +把如下代码添加到文件~/.bash_profile 或 ~/.bashrc中,然后source <~/.bash_profile|~/.bashrc>,或打开一个新终端,命令补全即生效 + +``` +complete -C $(which ucloud) ucloud +``` + +**Zsh shell** + +把如下代码添加到文件~/.zshrc中,然后source ~/.zshrc,或打开一个新终端,命令补全即生效 + +``` +autoload -U +X bashcompinit && bashcompinit +complete -F $(which ucloud) ucloud +``` +Zsh内置命令bashcompinit有可能在某些操作系统中不生效,如果以上脚本不生效,尝试用如下脚本替换 +``` +_ucloud() { + read -l; + local cl="$REPLY"; + read -ln; + local cp="$REPLY"; + reply=(`COMP_SHELL=zsh COMP_LINE="$cl" COMP_POINT="$cp" ucloud`) +} + +compctl -K _ucloud ucloud +``` + + +## 初始化配置 +UCloud CLI支持多个命名配置,这些配置存储在本地文件config.json和credential.json中,位于~/.ucloud目录。 +您可以使用```ucloud config add ```命令添加多个配置,使用--profile指定配置名称,或者直接在本地文件config.json和credential.json中添加配置。 +在本地没有已生效的配置的情况下,```ucloud init```命令会添加一个配置并命名为default,此命令尽可能简化了配置过程,适合第一次使用UCloud CLI的时候初始化配置。 + +总共有10个配置项 +- Profile: 配置名称, 此名称不允许重复。执行命令时可以被参数--profile覆盖 +- Active: 标识此配置是否生效,生效的配置只有一个 +- ProjectID: 默认项目ID,执行命令时可以被参数--project-id覆盖 +- Region: 默认地域,执行命令时可以被参数--region覆盖 +- Zone: 默认可用区,执行命令时可以被参数--zone覆盖 +- BaseURL: 默认的UCloud Open API地址,执行命令时可以被参数--base-url覆盖 +- Timeout: 默认的请求API超时时间,单位秒,执行命令是可以被参数--timeout覆盖 +- PublicKey: 账户公钥,执行命令时可以被参数--public-key覆盖 +- PrivateKey: 账户私钥,执行命令是可以被参数--private-key覆盖 +- MaxRetryTimes: 默认最大的API请求失败重试次数,只对幂等API生效,所谓幂API等是指不会因为多次调用而产生副作用,比如释放EIP(ReleaseEIP),执行命令时可以被参数--max-retry-times覆盖 + +添加或修改配置的命令如下 + +首次使用,初始化配置 +``` +$ ucloud init +``` +查看所有配置 +``` +$ ucloud config list + +Profile Active ProjectID Region Zone BaseURL Timeout PublicKey PrivateKey MaxRetryTimes +default true org-oxjwoi cn-bj2 cn-bj2-05 https://api.ucloud.cn/ 15 YSQGIZrL*****nCRQ= jtma2eqQ*****+Avms 3 +uweb false org-bdks4e cn-bj2 cn-bj2-05 https://api.ucloud.cn/ 15 4E9UU0Vh*****PWQ== 694581ea*****a0d45 3 +``` + +添加配置 +``` +$ ucloud config add --profile --public-key xxx --private-key xxx +``` + +修改某个配置的配置项 + +``` +$ ucloud config update --profile xxx --region cn-sh2 +``` + +更多信息,请参考命令帮助 +``` +$ ucloud config --help +``` + +## 举例说明 + +用UCloud CLI在尼日利亚创建数据中心创建一台主机并绑定一个外网IP,然后配置GlobalSSH加速,加速中国大陆到目的主机的SSH登陆 + +首先,创建云主机 +``` +$ ucloud uhost create --cpu 1 --memory-gb 1 --password **** --image-id uimage-fya3qr + +uhost[uhost-zbuxxxx] is initializing...done +``` + +*备注* + +执行以下命令查看创建主机命令的各参数含义 + +``` +$ ucloud uhost create --help +``` + +其次,申请一个EIP,然后绑定到刚刚创建的主机上 +Secondly, we're going to allocate an EIP and then bind it to the uhost created above. + +``` +$ ucloud eip allocate --bandwidth-mb 1 +allocate EIP[eip-xxx] IP:106.75.xx.xx Line:BGP + +$ ucloud eip bind --eip-id eip-xxx --resource-id uhost-xxx +bind EIP[eip-xxx] with uhost[uhost-xxx] +``` + +以上操作也可以用一个命令完成 +``` +$ ucloud uhost create --cpu 1 --memory-gb 1 --password **** --image-id uimage-fya3qr --create-eip-bandwidth-mb 1 +``` + +配置GlobalSSH,然后通过GlobalSSH登陆主机 + +``` +$ ucloud gssh create --location Washington --target-ip 152.32.140.92 +gssh[uga-0psxxx] created + +$ ssh root@152.32.140.92.ipssh.net +root@152.32.140.92.ipssh.net's password: password of the uhost instance +``` + +使用"ucloud api"命令调用任意API,根据API文档把某个API的参数依次填入。此命令比较特殊,不支持--public-key,--private-key,--debug,--profile,--timeout-sec等公共参数,如果要开启debug模式,可以设置环境变量$UCLOUD_CLI_DEBUG=on + +``` +$ ucloud api --Action --Param1 --Param2 ... +``` +或者把API参数写到JSON文件中,举例如下 +``` +$ ucloud api --local-file ./create_uhost.json + +//create_uhost.json文件内容 +{ + "Action":"CreateUHostInstance", + "Region":"cn-bj2", + "Zone":"cn-bj2-02", + "ImageId":"uimage-gk2x3x", + "NetworkInterface": [{ + "EIP":{ + "Bandwidth":1, + "OperatorName":"Bgp", + "PayMode": "Bandwidth" + } + }], + "LoginMode":"Password", + "Password":"dGVzdGx4ajEy", + "CPU":1, + "Memory":2048, + "Disks":[ + { + "Size":20, + "Type":"LOCAL_NORMAL", + "IsBoot":"true" + } + ] +} +``` \ No newline at end of file diff --git a/README.md b/README.md index a313d6fa8b..dfe88c9cf3 100644 --- a/README.md +++ b/README.md @@ -1,123 +1,263 @@ -## ucloud-cli - -- website: https://www.ucloud.cn/ +English | [简体中文](./README-CN.md) -![](http://cli-ucloud-logo.sg.ufileos.com/ucloud.png) +## UCloud CLI -The ucloud-cli provides a unified command line interface to manage Ucloud services. It works through Golang SDK based on UCloud OpenAPI and support Linux, macOS, and Windows. +![](./docs/_static/ucloud_cli_demo.gif) -## Installation +The UCloud CLI provides a unified command line interface to UCloud services. It works on [ucloud-sdk-go](https://github.com/ucloud/ucloud-sdk-go) based on UCloud OpenAPI and supports Linux, macOS and Windows. +https://docs.ucloud.cn/developer/cli/index -The easiest way to install ucloud-cli is to use home-brew for Linux and macOS users. This will install the package as well as all dependencies. +## Installing ucloud-cli on macOS or Linux + +**Using Homebrew(Recommended on macOS)** + +The [Homebrew](https://docs.brew.sh/Installation) package manager may be used on macOS and Linux. +It could install ucloud-cli and its dependencies automatically by running command below. ``` -$ brew install ucloud +brew install ucloud ``` -If you have the ucloud-cli installed and want to upgrade to the latest version you can run: +If you have installed ucloud-cli already and want to upgrade to the latest version, just run: ``` -$ brew upgrade ucloud +brew upgrade ucloud ``` -**Note** +If you come across some errors during the installation via homebrew, please update the homebrew first and try again. -If you come across error during the installation via home-brew, you may update the management package first. +``` +brew update +``` + +If the error is still unresolved, try the following command for help. ``` -$ brew update +brew doctor ``` -**Build from the source code** +**Building from source(Recommended if you have golang installed)** + +If you have installed git and golang on your platform, you can fetch the source code of ucloud cli from github and complie it by yourself. + +``` +git clone https://github.com/ucloud/ucloud-cli.git +cd ucloud-cli +make install +``` -For windows users, suggest build from the source code which require install Golang first. This also works for Linux and macOS. +Upgrade to latest version ``` -$ mkdir -p $GOPATH/src/github.com/ucloud -$ cd $GOPATH/src/github.com/ucloud -$ git clone https://github.com/ucloud/ucloud-cli.git -$ cd ucloud-cli -$ make install +cd ucloud-cli +git pull +make install ``` -## Command Completion +**Downloading binary release(Recommended on Linux)** -The ucloud-cli include command completion feature and need configure it manually. Add following scripts to ~/.bash_profile or ~/.bashrc +Visit the [releases page](https://github.com/ucloud/ucloud-cli/releases) of ucloud cli, and find the appropriate archive for your operating system and architecture. +Download the archive , check the shasum256 hashcode and extract it to your $PATH +For example ``` -complete -C /usr/local/bin/ucloud ucloud +curl -OL https://github.com/ucloud/ucloud-cli/releases/download/0.1.22/ucloud-cli-linux-0.1.22-amd64.tgz +echo "efbfb6d36d99f692b1f9cc7c9e3858047bb7b4fca6205c454098267e660b41d9 *ucloud-cli-linux-0.1.22-amd64.tgz" | shasum -c //check shasum to verify whether the downloaded tarball was hijacked. get the shasum from release page +tar zxf ucloud-cli-linux-0.1.22-amd64.tgz -C /usr/local/bin/ ``` -**Zsh shell** please add following scripts to ~/.zshrc +## Installing ucloud cli on Windows + +**Building from source** + +Download the source code of ucloud cli from [releases page](https://github.com/ucloud/ucloud-cli/releases) and extract it. You can also download it by running ```git clone https://github.com/ucloud/ucloud-cli.git``` +Go to the directory of the source code, and then compile the source code by running ```go build -mod=vendor -o ucloud.exe``` +After that add ucloud.exe to your environment variable PATH. You could follow [this document](https://www.java.com/en/download/help/path.xml) if you don't know how to do. +Open CMD Terminal and run ```ucloud --version ``` to test installation. + + +**Downloading binary release** + +Vist the [releases page](https://github.com/ucloud/ucloud-cli/releases) of ucloud cli, and find the appropriate archive for your operating system and architecture. +Download the archive , and extract it. Add binary file ucloud.exe to your environment variable PATH following [this document](https://www.java.com/en/download/help/path.xml) + +## Using ucloud cli in a Docker container +If you have installed docker on your platform, pull the docker image embedded ucloud cli by follow command. Lookup Dockerfile from [here](./Dockerfile) +``` +docker pull uhub.service.ucloud.cn/ucloudcli/ucloud-cli:source-code +``` + +Create a docker container named ucloud-cli using the docker image your have pulled by following command. + +``` +docker run --name ucloud-cli -it -d uhub.service.ucloud.cn/ucloudcli/ucloud-cli:source-code +``` +Run bash command in ucloud-cli container, and then you could play with ucloud cli. + +``` +docker exec -it ucloud-cli zsh +``` + +## Enabling Shell Auto-Completion for bash or zsh shell user. + +UCloud CLI also has auto-completion support. It can be set up so that if you partially type a command and then press TAB, the rest of the command is automatically filled in. + +**Bash shell** + +Add following scripts to ~/.bash_profile or ~/.bashrc and then restart your terminal or run ```source <~/.bash_profile|~/.bashrc>``` + +``` +complete -C $(which ucloud) ucloud +``` + +**Zsh shell** + +Add following scripts to ~/.zshrc and then restart your terminal or run ```source ~/.zshrc``` ``` autoload -U +X bashcompinit && bashcompinit -complete -F /usr/local/bin/ucloud ucloud +complete -F $(which ucloud) ucloud +``` +Zsh builtin command bashcompinit may not work on some platform. If the scripts don't work on your OS, try following scripts ``` +_ucloud() { + read -l; + local cl="$REPLY"; + read -ln; + local cp="$REPLY"; + reply=(`COMP_SHELL=zsh COMP_LINE="$cl" COMP_POINT="$cp" ucloud`) +} + +compctl -K _ucloud ucloud +``` + + +## Setup configuration -## Getting Started +The UCloud CLI supports using any of multiple named profiles that are stored in config.json and credential.json files which located in ~/.ucloud. +You can configure additional profiles by using ```ucloud config add``` with the --profile flag, or by adding entries to the config.json and credential.json files. +ucloud init will add profile named default if you do not have an active profile, and it does its best to reduce configuration items for first-time use of ucloud-cli. -Run the command to get started and configure ucloud-cli follow the steps. The public & private keys will be saved automatically and locally to directory ~/.ucloud. -You can delete the directory whenever you want. +There are 10 configuration items +- Profile: name of the profile, duplicated names are not allowed. It can be override by --profile flag +- Active: Whether to take effect, Only one profile is active +- ProjectID: ID of default project, and it can be override by --project-id flag +- Region: default region, it can be override by --region flag +- Zone: default zone, it can be override by --zone flag +- BaseURL: default url of UCloud Open API, it can be override by --base-url flag +- Timeout: default timeout value of querying UCloud Open API, unit second. It can be override by --timeout flag +- PublicKey: public key of your account. It can be override by --public-key flag +- PrivateKey: private key of your account. It can be override by --private-key flag +- MaxRetryTimes: default max retry times for failed API request. It only works for idempotent APIs which can be called many times without side effect, for example 'ReleaseEIP', and it can be override by --max-retry-times flag + +Run the command below to get started and configure ucloud-cli. ``` $ ucloud init ``` +List all profiles (for example) +``` +$ ucloud config list + +Profile Active ProjectID Region Zone BaseURL Timeout PublicKey PrivateKey MaxRetryTimes +default true org-oxjwoi cn-bj2 cn-bj2-05 https://api.ucloud.cn/ 15 YSQGIZrL*****nCRQ= jtma2eqQ*****+Avms 3 +uweb false org-bdks4e cn-bj2 cn-bj2-05 https://api.ucloud.cn/ 15 4E9UU0Vh*****PWQ== 694581ea*****a0d45 3 +``` + +Add additional profiles +``` +$ ucloud config add --profile --public-key xxx --private-key xxx +``` -To reset the configurations, run the command: +To change configuration items of specified profile, run: ``` -$ ucloud config +$ ucloud config update --profile xxx --region cn-sh2 ``` -To learn the usage and flags, run the command: +For more information, run: ``` -$ ucloud help +$ ucloud config --help ``` -## Example +## For example -Taking create uhost in Nigeria (region: air-nigeria) and bind a public IP as an example, then configure GlobalSSH to accelerate the SSH efficiency beyond China mainland. +I want to create a uhost in Nigeria (region: air-nigeria) and bind a public IP, and then configure GlobalSSH to accelerate efficiency of SSH service beyond China mainland. -First to create an uhost instance: +Firstly, create an uhost instance: ``` -$ ucloud uhost create --cpu 1 --memory 1 --password mypassword123 --image-id uimage-fya3qr +$ ucloud uhost create --cpu 1 --memory-gb 1 --password **** --image-id uimage-fya3qr -UHost:[uhost-tr1eau] created successfully! +uhost[uhost-zbuxxxx] is initializing...done ``` *Note* -Run follow command to get details regarding the parameters to create new uhost instance. +Run command below to get details about the parameters of creating new uhost instance. ``` $ ucloud uhost create --help ``` -And suggest run the command to get the image-id first. +Secondly, we're going to allocate an EIP and then bind it to the uhost created above. ``` -$ ucloud image list -``` - -Secondly, we're going to allocate an EIP and bind to the instance created above. +$ ucloud eip allocate --bandwidth-mb 1 +allocate EIP[eip-xxx] IP:106.75.xx.xx Line:BGP +$ ucloud eip bind --eip-id eip-xxx --resource-id uhost-xxx +bind EIP[eip-xxx] with uhost[uhost-xxx] ``` -$ ucloud eip allocate --line International --bandwidth 1 -EIPId:eip-xov13b,IP:152.32.140.92,Line:International -$ ucloud eip bind --eip-id eip-xov13b --resource-id uhost-tr1eau -EIP: [eip-xov13b] bind with uhost:[uhost-tr1eau] successfully +The operations above also can be done by one command +``` +$ ucloud uhost create --cpu 1 --memory-gb 1 --password **** --image-id uimage-fya3qr --create-eip-bandwidth-mb 1 ``` Configure the GlobalSSH to the uhost instance and login the instance via GlobalSSH ``` $ ucloud gssh create --location Washington --target-ip 152.32.140.92 -ResourceID: uga-pdhxvs +gssh[uga-0psxxx] created $ ssh root@152.32.140.92.ipssh.net root@152.32.140.92.ipssh.net's password: password of the uhost instance ``` + +Using command "ucloud api" to call any API.Fill in the parameters of an API in sequence according to the API documentation. This command is quite special, and public parameters such as --public-key,--private-key,--debug,--profile,--timeout-sec are not supported. If you want to tune on debug mode, set environment variable $UCLOUD_CLI_DEBUG=on + +``` +$ ucloud api --Action --Param1 --Param2 ... +``` +You can also put those API parameters into a json file, like this. +``` +$ ucloud api --local-file ./create_uhost.json + +//content of file create_uhost.json +{ + "Action":"CreateUHostInstance", + "Region":"cn-bj2", + "Zone":"cn-bj2-02", + "ImageId":"uimage-gk2x3x", + "NetworkInterface": [{ + "EIP":{ + "Bandwidth":1, + "OperatorName":"Bgp", + "PayMode": "Bandwidth" + } + }], + "LoginMode":"Password", + "Password":"dGVzdGx4ajEy", + "CPU":1, + "Memory":2048, + "Disks":[ + { + "Size":20, + "Type":"LOCAL_NORMAL", + "IsBoot":"true" + } + ] +} +``` diff --git a/ansi/code.go b/ansi/code.go index 0f09f094fa..22956a2525 100644 --- a/ansi/code.go +++ b/ansi/code.go @@ -1,18 +1,34 @@ -// Reference https://github.com/sindresorhus/ansi-escapes +// Package ansi reference https://github.com/sindresorhus/ansi-escapes package ansi import ( "fmt" ) -const ESC = "\x1b[" -const OSC = "\x1b]" -const BEL = "\x07" -const SEP = ";" +const csi = "\x1b[" -var CursorLeft = fmt.Sprintf("%sG", ESC) -var EraseDown = fmt.Sprintf("%sJ", ESC) +const sep = ";" +// CursorLeft move cursor to the left side +var CursorLeft = fmt.Sprintf("%sG", csi) + +// EraseDown Erase the screen from the current line down to the bottom of the +var EraseDown = fmt.Sprintf("%sJ", csi) + +// EraseUp Erase the screen from the current line up to the top of the screen +var EraseUp = fmt.Sprintf("%s1J", csi) + +// CursorUp Move cursor up a specific amount of rows. func CursorUp(count int) string { - return fmt.Sprintf("%s%dA", ESC, count) + return fmt.Sprintf("%s%dA", csi, count) +} + +// CursorPrevLine Move cursor up a specific amount of rows. +func CursorPrevLine(count int) string { + return fmt.Sprintf("%s%dF", csi, count) +} + +// CursorTo Set the absolute position of the cursor. `x` `y` is the top left of the screen. +func CursorTo(x, y int) string { + return fmt.Sprintf("%s%d;%dH", csi, y+1, x+1) } diff --git a/base/client.go b/base/client.go index ecd923827f..d6d0745931 100644 --- a/base/client.go +++ b/base/client.go @@ -1,37 +1,164 @@ package base import ( + "github.com/ucloud/ucloud-sdk-go/private/protocol/http" + ppathx "github.com/ucloud/ucloud-sdk-go/private/services/pathx" + pudb "github.com/ucloud/ucloud-sdk-go/private/services/udb" + puhost "github.com/ucloud/ucloud-sdk-go/private/services/uhost" + pumem "github.com/ucloud/ucloud-sdk-go/private/services/umem" "github.com/ucloud/ucloud-sdk-go/services/pathx" "github.com/ucloud/ucloud-sdk-go/services/uaccount" + "github.com/ucloud/ucloud-sdk-go/services/ucompshare" + "github.com/ucloud/ucloud-sdk-go/services/udb" "github.com/ucloud/ucloud-sdk-go/services/udisk" + "github.com/ucloud/ucloud-sdk-go/services/udpn" "github.com/ucloud/ucloud-sdk-go/services/uhost" "github.com/ucloud/ucloud-sdk-go/services/ulb" + "github.com/ucloud/ucloud-sdk-go/services/umem" "github.com/ucloud/ucloud-sdk-go/services/unet" + "github.com/ucloud/ucloud-sdk-go/services/uphost" "github.com/ucloud/ucloud-sdk-go/services/vpc" - "github.com/ucloud/ucloud-sdk-go/ucloud" + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" "github.com/ucloud/ucloud-sdk-go/ucloud/auth" + "github.com/ucloud/ucloud-sdk-go/ucloud/request" ) -//Client aggregate client for business +// PrivateUHostClient 私有模块的uhost client 即未在官网开放的接口 +type PrivateUHostClient = puhost.UHostClient + +// PrivateUDBClient 私有模块的udb client 即未在官网开放的接口 +type PrivateUDBClient = pudb.UDBClient + +// PrivateUMemClient 私有模块的umem client 即未在官网开放的接口 +type PrivateUMemClient = pumem.UMemClient + +// PrivatePathxClient 私有模块的pathx client 即未在官网开放的接口 +type PrivatePathxClient = ppathx.PathXClient + +// Client aggregate client for business type Client struct { uaccount.UAccountClient uhost.UHostClient unet.UNetClient - ulb.ULBClient vpc.VPCClient + udpn.UDPNClient pathx.PathXClient udisk.UDiskClient + ulb.ULBClient + udb.UDBClient + umem.UMemClient + uphost.UPHostClient + PrivateUHostClient + PrivateUDBClient + PrivateUMemClient PrivateUMemClient + PrivatePathxClient + ucompshare.UCompShareClient } // NewClient will return a aggregate client -func NewClient(config *ucloud.Config, credential *auth.Credential) *Client { +func NewClient(config *sdk.Config, credConfig *CredentialConfig) *Client { + var handler sdk.RequestHandler = func(c *sdk.Client, req request.Common) (request.Common, error) { + err := req.SetProjectId(PickResourceID(req.GetProjectId())) + return req, err + } + var injectCredHeader sdk.HttpRequestHandler = func(c *sdk.Client, req *http.HttpRequest) (*http.HttpRequest, error) { + err := req.SetHeader("Cookie", credConfig.Cookie) + if err != nil { + return req, err + } + err = req.SetHeader("Csrf-Token", credConfig.CSRFToken) + if err != nil { + return req, err + } + return req, err + } + credential := &auth.Credential{ + PublicKey: credConfig.PublicKey, + PrivateKey: credConfig.PrivateKey, + } + var ( + uaccountClient = *uaccount.NewClient(config, credential) + uhostClient = *uhost.NewClient(config, credential) + unetClient = *unet.NewClient(config, credential) + vpcClient = *vpc.NewClient(config, credential) + udpnClient = *udpn.NewClient(config, credential) + pathxClient = *pathx.NewClient(config, credential) + udiskClient = *udisk.NewClient(config, credential) + ulbClient = *ulb.NewClient(config, credential) + udbClient = *udb.NewClient(config, credential) + umemClient = *umem.NewClient(config, credential) + uphostClient = *uphost.NewClient(config, credential) + puhostClient = *puhost.NewClient(config, credential) + pudbClient = *pudb.NewClient(config, credential) + pumemClient = *pumem.NewClient(config, credential) + ppathxClient = *ppathx.NewClient(config, credential) + ulhostClient = *ucompshare.NewClient(config, credential) + ) + + uaccountClient.Client.AddRequestHandler(handler) + uaccountClient.Client.AddHttpRequestHandler(injectCredHeader) + + uhostClient.Client.AddRequestHandler(handler) + uhostClient.Client.AddHttpRequestHandler(injectCredHeader) + + unetClient.Client.AddRequestHandler(handler) + unetClient.Client.AddHttpRequestHandler(injectCredHeader) + + vpcClient.Client.AddRequestHandler(handler) + vpcClient.Client.AddHttpRequestHandler(injectCredHeader) + + udpnClient.Client.AddRequestHandler(handler) + udpnClient.Client.AddHttpRequestHandler(injectCredHeader) + + pathxClient.Client.AddRequestHandler(handler) + pathxClient.Client.AddHttpRequestHandler(injectCredHeader) + + udiskClient.Client.AddRequestHandler(handler) + udiskClient.Client.AddHttpRequestHandler(injectCredHeader) + + ulbClient.Client.AddRequestHandler(handler) + ulbClient.Client.AddHttpRequestHandler(injectCredHeader) + + udbClient.Client.AddRequestHandler(handler) + udbClient.Client.AddHttpRequestHandler(injectCredHeader) + + umemClient.Client.AddRequestHandler(handler) + umemClient.Client.AddHttpRequestHandler(injectCredHeader) + + uphostClient.Client.AddRequestHandler(handler) + uphostClient.Client.AddHttpRequestHandler(injectCredHeader) + + puhostClient.Client.AddRequestHandler(handler) + puhostClient.Client.AddHttpRequestHandler(injectCredHeader) + + pudbClient.Client.AddRequestHandler(handler) + pudbClient.Client.AddHttpRequestHandler(injectCredHeader) + + pumemClient.Client.AddRequestHandler(handler) + pumemClient.Client.AddHttpRequestHandler(injectCredHeader) + + ppathxClient.Client.AddRequestHandler(handler) + ppathxClient.Client.AddHttpRequestHandler(injectCredHeader) + + ulhostClient.Client.AddRequestHandler(handler) + ulhostClient.Client.AddHttpRequestHandler(injectCredHeader) + return &Client{ - *uaccount.NewClient(config, credential), - *uhost.NewClient(config, credential), - *unet.NewClient(config, credential), - *ulb.NewClient(config, credential), - *vpc.NewClient(config, credential), - *pathx.NewClient(config, credential), - *udisk.NewClient(config, credential), + uaccountClient, + uhostClient, + unetClient, + vpcClient, + udpnClient, + pathxClient, + udiskClient, + ulbClient, + udbClient, + umemClient, + uphostClient, + puhostClient, + pudbClient, + pumemClient, + ppathxClient, + ulhostClient, } } diff --git a/base/config.go b/base/config.go index 62bfb0622b..b4ecb7f72d 100644 --- a/base/config.go +++ b/base/config.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "net/url" "os" "strings" "time" @@ -11,92 +12,175 @@ import ( "github.com/ucloud/ucloud-sdk-go/services/uaccount" sdk "github.com/ucloud/ucloud-sdk-go/ucloud" "github.com/ucloud/ucloud-sdk-go/ucloud/auth" + "github.com/ucloud/ucloud-sdk-go/ucloud/log" ) -const configFile = "config.json" +// ConfigFilePath path of config.json +var ConfigFilePath = fmt.Sprintf("%s/%s", GetConfigDir(), "config.json") -//Version 版本号 -const Version = "0.1.5" +// CredentialFilePath path of credential.json +var CredentialFilePath = fmt.Sprintf("%s/%s", GetConfigDir(), "credential.json") -//ConfigPath 配置文件路径 +var CredentialFilePathInCloudShell = os.Getenv("CLOUD_SHELL_CREDENTIAL_FILE") -//ConfigInstance 配置实例, 程序加载时生成 -var ConfigInstance = &Config{} +// LocalFileMode file mode of $HOME/ucloud/* +const LocalFileMode os.FileMode = 0600 -//ClientConfig 创建sdk client参数 +// DefaultTimeoutSec default timeout for requesting api, 15s +const DefaultTimeoutSec = 15 + +// DefaultMaxRetryTimes default timeout for requesting api, 15s +const DefaultMaxRetryTimes = 3 + +// DefaultBaseURL location of api server +const DefaultBaseURL = "https://api.ucloud.cn/" + +// DefaultProfile name of default profile +const DefaultProfile = "default" + +// Version 版本号 +const Version = "0.3.0" + +var UserAgent = fmt.Sprintf("UCloud-CLI/%s", Version) + +var InCloudShell = os.Getenv("CLOUD_SHELL") == "true" + +// ConfigIns 配置实例, 程序加载时生成 +var ConfigIns = &AggConfig{ + Profile: DefaultProfile, + BaseURL: DefaultBaseURL, + Timeout: DefaultTimeoutSec, + MaxRetryTimes: sdk.Int(DefaultMaxRetryTimes), +} + +// AggConfigListIns 配置列表, 进程启动时从本地文件加载 +var AggConfigListIns = &AggConfigManager{} + +// ClientConfig 创建sdk client参数 var ClientConfig *sdk.Config -//Credential 创建sdk client参数 -var Credential *auth.Credential +// AuthCredential 创建sdk client参数 +var AuthCredential *CredentialConfig + +// BizClient 用于调用业务接口 +var BizClient *Client + +// Global 全局flag +var Global GlobalFlag + +// GlobalFlag 几乎所有接口都需要的参数,例如 region zone projectID +type GlobalFlag struct { + Debug bool + JSON bool + Version bool + Completion bool + Config bool + Signup bool + Profile string + PublicKey string + PrivateKey string + BaseURL string + Timeout int + MaxRetryTimes int +} -// Config 全局配置 -type Config struct { +// CLIConfig cli_config element +type CLIConfig struct { + ProjectID string `json:"project_id"` + Region string `json:"region"` + Zone string `json:"zone"` + BaseURL string `json:"base_url"` + Timeout int `json:"timeout_sec"` + Profile string `json:"profile"` + Active bool `json:"active"` //是否生效 + MaxRetryTimes *int `json:"max_retry_times"` + AgreeUploadLog bool `json:"agree_upload_log"` +} + +// CredentialConfig credential element +type CredentialConfig struct { PublicKey string `json:"public_key"` PrivateKey string `json:"private_key"` - Region string `json:"region"` - Zone string `json:"zone"` - ProjectID string `json:"project_id"` + Cookie string `json:"cookie"` + CSRFToken string `json:"csrf_token"` + Profile string `json:"profile"` +} + +// AggConfig 聚合配置 config+credential +type AggConfig struct { + Profile string `json:"profile"` + Active bool `json:"active"` + ProjectID string `json:"project_id"` + Region string `json:"region"` + Zone string `json:"zone"` + BaseURL string `json:"base_url"` + Timeout int `json:"timeout_sec"` + PublicKey string `json:"public_key"` + PrivateKey string `json:"private_key"` + Cookie string `json:"cookie"` + CSRFToken string `json:"csrf_token"` + MaxRetryTimes *int `json:"max_retry_times"` + AgreeUploadLog bool `json:"agree_upload_log"` } -//ConfigPublicKey 输入公钥 -func (p *Config) ConfigPublicKey() error { +// ConfigPublicKey 输入公钥 +func (p *AggConfig) ConfigPublicKey() error { Cxt.Print("Your public-key:") _, err := fmt.Scanf("%s\n", &p.PublicKey) - p.PublicKey = strings.TrimSpace(p.PublicKey) - Credential.PublicKey = p.PublicKey - p.SaveConfig() if err != nil { Cxt.Println(err) + return err } - return err + p.PublicKey = strings.TrimSpace(p.PublicKey) + AuthCredential.PublicKey = p.PublicKey + return nil } -//ConfigPrivateKey 输入私钥 -func (p *Config) ConfigPrivateKey() error { +// ConfigPrivateKey 输入私钥 +func (p *AggConfig) ConfigPrivateKey() error { Cxt.Print("Your private-key:") _, err := fmt.Scanf("%s\n", &p.PrivateKey) - p.PrivateKey = strings.TrimSpace(p.PrivateKey) - Credential.PrivateKey = p.PrivateKey - p.SaveConfig() if err != nil { Cxt.Println(err) + return err } - return err + p.PrivateKey = strings.TrimSpace(p.PrivateKey) + AuthCredential.PrivateKey = p.PrivateKey + return nil } -//ConfigRegion 输入默认Region -func (p *Config) ConfigRegion() error { - p.LoadConfig() - Cxt.Print("Default region:") - _, err := fmt.Scanf("%s\n", &p.Region) +// ConfigBaseURL 输入BaseURL +func (p *AggConfig) ConfigBaseURL() error { + fmt.Printf("Default base-url(%s):", DefaultBaseURL) + _, err := fmt.Scanf("%s\n", &p.BaseURL) if err != nil { - Cxt.PrintErr(err) return err } - p.Region = strings.TrimSpace(p.Region) - ClientConfig.Region = p.Region - p.SaveConfig() + p.BaseURL = strings.TrimSpace(p.BaseURL) + if len(p.BaseURL) == 0 { + p.BaseURL = DefaultBaseURL + } return nil } -//ConfigProjectID 输入默认ProjectID -func (p *Config) ConfigProjectID() error { - p.LoadConfig() - Cxt.Print("Default project-id:") - _, err := fmt.Scanf("%s\n", &p.ProjectID) +// ConfigUploadLog agree upload log or not +func (p *AggConfig) ConfigUploadLog() error { + var input string + fmt.Print("Do you agree to upload log in local file ~/.ucloud/cli.log to help ucloud-cli get better(yes|no):") + _, err := fmt.Scanf("%s\n", &input) if err != nil { - Cxt.Println(err) + HandleError(err) return err } - p.ProjectID = strings.TrimSpace(p.ProjectID) - ClientConfig.ProjectId = p.ProjectID - p.SaveConfig() + + if str := strings.ToLower(input); str == "y" || str == "ye" || str == "yes" { + p.AgreeUploadLog = true + } return nil } -//GetClientConfig 用来生成sdkClient -func (p *Config) GetClientConfig(isDebug bool) *sdk.Config { - p.LoadConfig() +// GetClientConfig 用来生成sdkClient +func (p *AggConfig) GetClientConfig(isDebug bool) *sdk.Config { clientConfig := &sdk.Config{ Region: p.Region, ProjectId: p.ProjectID, @@ -106,72 +190,436 @@ func (p *Config) GetClientConfig(isDebug bool) *sdk.Config { LogLevel: ClientConfig.LogLevel, } if isDebug == true { - clientConfig.LogLevel = 5 + clientConfig.LogLevel = log.DebugLevel } return clientConfig } -//GetCredential 用来生成SDkClient -func (p *Config) GetCredential() *auth.Credential { - p.LoadConfig() +// GetCredential 用来生成SDkClient +func (p *AggConfig) GetCredential() *auth.Credential { return &auth.Credential{ PublicKey: p.PublicKey, PrivateKey: p.PrivateKey, } } -//ListConfig 查看配置 -func (p *Config) ListConfig(json bool) error { - tmpConfig := *p - tmpConfig.PrivateKey = MosaicString(tmpConfig.PrivateKey, 8, 5) - tmpConfig.PublicKey = MosaicString(tmpConfig.PublicKey, 8, 5) +func (p *AggConfig) copyToCLIConfig(target *CLIConfig) { + target.Profile = p.Profile + target.BaseURL = p.BaseURL + target.Timeout = p.Timeout + target.ProjectID = p.ProjectID + target.Region = p.Region + target.Zone = p.Zone + target.Active = p.Active + target.MaxRetryTimes = p.MaxRetryTimes + target.AgreeUploadLog = p.AgreeUploadLog +} - if json { - return PrintJSON(tmpConfig) +func (p *AggConfig) copyToCredentialConfig(target *CredentialConfig) { + target.Profile = p.Profile + target.PrivateKey = p.PrivateKey + target.PublicKey = p.PublicKey + target.Cookie = p.Cookie + target.CSRFToken = p.CSRFToken +} + +// AggConfigManager 配置管理 +type AggConfigManager struct { + activeProfile string + configs map[string]*AggConfig + configFile *os.File + credFile *os.File +} + +// NewAggConfigManager create instance +func NewAggConfigManager(cfgFile, credFile *os.File) (*AggConfigManager, error) { + manager := &AggConfigManager{ + configs: make(map[string]*AggConfig), + configFile: cfgFile, + credFile: credFile, } - PrintTable([]Config{tmpConfig}, []string{"PublicKey", "PrivateKey", "Region", "Zone", "ProjectID"}) - return nil + + err := manager.Load() + if err != nil { + if !os.IsNotExist(err) { + return manager, err + } + + aerr := adaptOldConfig() + if aerr != nil { + HandleError(fmt.Errorf("adapt to old config failed: %v", aerr)) + return manager, aerr + } + + err := manager.Load() + if err != nil { + HandleError(fmt.Errorf("retry to load cli config failed: %v", err)) + return manager, err + } + } + return manager, nil +} + +// Append config to list, override if already exist the same profile +func (p *AggConfigManager) Append(config *AggConfig) error { + if _, ok := p.configs[config.Profile]; ok { + return fmt.Errorf("profile [%s] exists already", config.Profile) + } + + if config.Active && config.Profile != p.activeProfile { + if ac, ok := p.configs[p.activeProfile]; ok { + ac.Active = false + } + p.activeProfile = config.Profile + } + p.configs[config.Profile] = config + return p.Save() } -//ClearConfig 清空配置 -func (p *Config) ClearConfig() error { - p = &Config{} - return p.SaveConfig() +// UpdateAggConfig update AggConfig append if not exist +func (p *AggConfigManager) UpdateAggConfig(config *AggConfig) error { + if _, ok := p.configs[config.Profile]; !ok { + return p.Append(config) + } + + if config.Active && config.Profile != p.activeProfile { + if ac, ok := p.configs[p.activeProfile]; ok { + ac.Active = false + } + p.activeProfile = config.Profile + } + return p.Save() } -//SaveConfig 保存配置到本地文件,以后可以直接使用 -func (p *Config) SaveConfig() error { - bytes, err := json.Marshal(p) +// Load AggConfigList from local file $HOME/.ucloud/config.json+credential.json +func (p *AggConfigManager) Load() error { + configs, err := p.parseCLIConfigs() if err != nil { - return err + return fmt.Errorf("read config failed: %v", err) } - fileFullPath := GetConfigPath() + "/" + configFile - err = ioutil.WriteFile(fileFullPath, bytes, 0600) - return err + credentials, err := p.parseCredentials() + if err != nil { + return fmt.Errorf("read credential failed: %v", err) + } + + //key: profile , value: CLIConfig + configMap := make(map[string]*CLIConfig) + for _, config := range configs { + c := config + configMap[config.Profile] = &c + if config.Active { + p.activeProfile = config.Profile + } + } + credMap := make(map[string]*CredentialConfig) + for _, cred := range credentials { + c := cred + credMap[cred.Profile] = &c + } + + for profile, config := range configMap { + cred, ok := credMap[profile] + if !ok { + LogError("profile: %s don't exist in credential") + continue + } + + p.configs[profile] = &AggConfig{ + PrivateKey: cred.PrivateKey, + PublicKey: cred.PublicKey, + Cookie: cred.Cookie, + CSRFToken: cred.CSRFToken, + Profile: config.Profile, + ProjectID: config.ProjectID, + Region: config.Region, + Zone: config.Zone, + BaseURL: config.BaseURL, + Timeout: config.Timeout, + Active: config.Active, + MaxRetryTimes: config.MaxRetryTimes, + AgreeUploadLog: config.AgreeUploadLog, + } + } + + if p.activeProfile == "" && len(configMap) > 0 { + return fmt.Errorf("no active config found, run 'ucloud config list' to check") + } + if _, ok := credMap[p.activeProfile]; p.activeProfile != "" && !ok { + return fmt.Errorf("profile %s's credential don't exist, run 'ucloud config list' to check", p.activeProfile) + } + + return nil +} + +type CredHeader struct { + Key string + Value []string } -//LoadConfig 从本地文件加载配置 -func (p *Config) LoadConfig() error { - fileFullPath := GetConfigPath() + "/" + configFile - if _, err := os.Stat(fileFullPath); os.IsNotExist(err) { - p = new(Config) +type project struct { + ProjectId string + ProjectName string +} + +type region struct { + Region string + Zone string +} + +func NewInCloudShell() (*AggConfigManager, error) { + credFile, err := os.OpenFile(CredentialFilePathInCloudShell, os.O_RDONLY, LocalFileMode) + if err != nil { + return nil, fmt.Errorf("open credential file error: %w", err) + } + data, err := ioutil.ReadAll(credFile) + if err != nil { + return nil, fmt.Errorf("read from credential file error: %w", err) + } + var creds []CredHeader + err = json.Unmarshal(data, &creds) + if err != nil { + return nil, fmt.Errorf("unmarshal credential file error: %w", err) + } + + var cookie string + var tokenMap map[string]string + for _, header := range creds { + key := strings.ToLower(header.Key) + if key == "cookie" { + cookie = header.Value[0] + tokenMap, err = parseCookie(header.Value[0]) + } + } + if err != nil { + return nil, err + } + email := tokenMap["U_USER_EMAIL"] + email = strings.ReplaceAll(email, ".", "_") + email = strings.ReplaceAll(email, "@", "_") + projectKey := fmt.Sprintf("c_project_%s", email) + regionKey := fmt.Sprintf("c_last_region_%s", email) + var proj project + var reg region + if _, ok := tokenMap[projectKey]; ok { + err = json.Unmarshal([]byte(tokenMap[projectKey]), &proj) + if err != nil { + return nil, err + } } else { - content, err := ioutil.ReadFile(fileFullPath) + id, name, err := getDefaultProject(cookie, tokenMap["CSRF_TOKEN"]) if err != nil { - return err + return nil, fmt.Errorf("query default project error: %w", err) + } + proj.ProjectId = id + proj.ProjectName = name + } + if _, ok := tokenMap[regionKey]; ok { + err = json.Unmarshal([]byte(tokenMap[regionKey]), ®) + if err != nil { + return nil, err + } + } else { + region, zone, err := getDefaultRegion(cookie, tokenMap["CSRF_TOKEN"]) + if err != nil { + return nil, fmt.Errorf("query default region error: %w", err) } - json.Unmarshal(content, p) + reg.Region = region + reg.Zone = zone + } + + ac := &AggConfig{ + Cookie: cookie, + Profile: DefaultProfile, + Active: true, + BaseURL: DefaultBaseURL, + ProjectID: proj.ProjectId, + Region: reg.Region, + Zone: reg.Zone, + MaxRetryTimes: sdk.Int(DefaultMaxRetryTimes), + CSRFToken: tokenMap["CSRF_TOKEN"], + Timeout: DefaultTimeoutSec, + } + + aggConfigs := make(map[string]*AggConfig, 0) + aggConfigs[DefaultProfile] = ac + + return &AggConfigManager{ + activeProfile: DefaultProfile, + configs: aggConfigs, + }, nil +} + +func parseCookie(str string) (map[string]string, error) { + items := strings.Split(str, ";") + tokenMap := make(map[string]string, 0) + for _, str := range items { + strs := strings.SplitN(str, "=", 2) + if len(strs) == 2 { + v, err := url.QueryUnescape(strings.TrimSpace(strs[1])) + if err != nil { + return tokenMap, err + } + tokenMap[strings.TrimSpace(strs[0])] = v + } + } + return tokenMap, nil +} + +// Save configs to local file +func (p *AggConfigManager) Save() error { + var clics []*CLIConfig + var credcs []*CredentialConfig + for _, aggConfig := range p.configs { + cliConfig := &CLIConfig{} + aggConfig.copyToCLIConfig(cliConfig) + clics = append(clics, cliConfig) + + credConfig := &CredentialConfig{} + aggConfig.copyToCredentialConfig(credConfig) + credcs = append(credcs, credConfig) + } + aerr := WriteJSONFile(clics, p.configFile.Name()) + berr := WriteJSONFile(credcs, p.credFile.Name()) + + if aerr != nil && berr != nil { + return fmt.Errorf("save cli config failed: %v | save credentail failed: %v", aerr, berr) + } + if aerr != nil { + return fmt.Errorf("save cli config failed: %v", aerr) + } + if berr != nil { + return fmt.Errorf("save cerdentail failed: %v", berr) + } + return nil +} + +// DeleteByProfile 从AggConfigList和本地文件中删除此配置 +func (p *AggConfigManager) DeleteByProfile(profile string) error { + if _, ok := p.configs[profile]; !ok { + return fmt.Errorf("profile: %s is not exist", profile) + } + + ac := p.configs[profile] + if ac.Active { + return fmt.Errorf("can't delete active profile") + } + + delete(p.configs, profile) + + err := p.Save() + if err != nil { + return fmt.Errorf("delete profile %s failed: %v", profile, err) } return nil } -//LoadUserInfo 从~/.ucloud/user.json加载用户信息 +// GetProfileNameList 获取所有profiles 用于ucloud config --profile 补全 +func (p *AggConfigManager) GetProfileNameList() []string { + profiles := []string{} + for _, item := range p.configs { + profiles = append(profiles, item.Profile) + } + return profiles +} + +// GetAggConfigList get all profile config +func (p *AggConfigManager) GetAggConfigList() []AggConfig { + configs := []AggConfig{} + for _, cfg := range p.configs { + configs = append(configs, *cfg) + } + return configs +} + +// GetAggConfigByProfile get config of specify profile +func (p *AggConfigManager) GetAggConfigByProfile(profile string) (*AggConfig, bool) { + if ac, ok := p.configs[profile]; ok { + return ac, true + } + return nil, false +} + +// GetActiveAggConfig get active agg config +func (p *AggConfigManager) GetActiveAggConfig() (*AggConfig, error) { + if ac, ok := p.configs[p.activeProfile]; ok { + return ac, nil + } + return nil, fmt.Errorf("active profile not found. see 'ucloud config list'") +} + +// GetActiveAggConfigName get active config name +func (p *AggConfigManager) GetActiveAggConfigName() string { + if ac, ok := p.configs[p.activeProfile]; ok { + return ac.Profile + } + return "" +} + +func (p *AggConfigManager) parseCLIConfigs() ([]CLIConfig, error) { + var configs []CLIConfig + rawConfig, err := ioutil.ReadAll(p.configFile) + if err != nil { + return nil, err + } + if len(rawConfig) == 0 { + return nil, nil + } + + err = json.Unmarshal(rawConfig, &configs) + if err != nil { + return nil, fmt.Errorf("parse cli config faild: %v", err) + } + //特殊处理未配置max_retry_times的情况,v0.1.21之前硬编码重试次数为3 + for idx := range configs { + if configs[idx].MaxRetryTimes == nil { + configs[idx].MaxRetryTimes = sdk.Int(DefaultMaxRetryTimes) + } + } + return configs, nil +} + +func (p *AggConfigManager) parseCredentials() ([]CredentialConfig, error) { + var credentials []CredentialConfig + rawCred, err := ioutil.ReadAll(p.credFile) + if err != nil { + return nil, err + } + + if len(rawCred) == 0 { + return nil, nil + } + + err = json.Unmarshal(rawCred, &credentials) + if err != nil { + return nil, fmt.Errorf("parse credential failed: %v", err) + } + return credentials, nil +} + +// ListAggConfig ucloud --config + ucloud config list +func ListAggConfig(json bool) { + aggConfigs := AggConfigListIns.GetAggConfigList() + for idx, ac := range aggConfigs { + aggConfigs[idx].PrivateKey = MosaicString(ac.PrivateKey, 8, 5) + aggConfigs[idx].PublicKey = MosaicString(ac.PublicKey, 8, 5) + } + if json { + err := PrintJSON(aggConfigs, os.Stdout) + if err != nil { + HandleError(err) + } + } else { + PrintTable(aggConfigs, []string{"Profile", "Active", "ProjectID", "Region", "Zone", "BaseURL", "Timeout", "PublicKey", "PrivateKey", "MaxRetryTimes", "AgreeUploadLog"}) + } +} + +// LoadUserInfo 从~/.ucloud/user.json加载用户信息 func LoadUserInfo() (*uaccount.UserInfo, error) { - fileFullPath := GetConfigPath() + "/user.json" - if _, err := os.Stat(fileFullPath); os.IsNotExist(err) { - return new(uaccount.UserInfo), nil + filePath := GetConfigDir() + "/user.json" + if _, err := os.Stat(filePath); os.IsNotExist(err) { + return nil, fmt.Errorf("user.json is not exist") } - content, err := ioutil.ReadFile(fileFullPath) + content, err := ioutil.ReadFile(filePath) if err != nil { return nil, err } @@ -183,25 +631,225 @@ func LoadUserInfo() (*uaccount.UserInfo, error) { return &user, nil } -func init() { - ConfigInstance.LoadConfig() - timeout, _ := time.ParseDuration("15s") +// GetUserInfo from local file and remote api +func GetUserInfo() (*uaccount.UserInfo, error) { + user, err := LoadUserInfo() + if err == nil { + return user, nil + } + + req := BizClient.NewGetUserInfoRequest() + resp, err := BizClient.GetUserInfo(req) + + if err != nil { + return nil, err + } + + if len(resp.DataSet) == 1 { + user = &resp.DataSet[0] + bytes, err := json.Marshal(user) + if err != nil { + return nil, err + } + fileFullPath := GetConfigDir() + "/user.json" + err = ioutil.WriteFile(fileFullPath, bytes, 0600) + if err != nil { + return nil, err + } + } else { + return nil, fmt.Errorf("GetUserInfo DataSet length: %d", len(resp.DataSet)) + } + return user, nil +} + +// OldConfig 0.1.7以及之前版本的配置struct +type OldConfig struct { + PublicKey string `json:"public_key"` + PrivateKey string `json:"private_key"` + Region string `json:"region"` + Zone string `json:"zone"` + ProjectID string `json:"project_id"` +} + +// Load 从本地文件加载配置 +func (p *OldConfig) Load() error { + if _, err := os.Stat(ConfigFilePath); os.IsNotExist(err) { + p = new(OldConfig) + return nil + } + + content, err := ioutil.ReadFile(ConfigFilePath) + if err != nil { + return err + } + err = json.Unmarshal(content, p) + if err != nil { + return err + } + + return nil +} + +func adaptOldConfig() error { + oc := &OldConfig{} + err := oc.Load() + if err != nil { + return err + } + ac := &AggConfig{ + Profile: DefaultProfile, + ProjectID: oc.ProjectID, + Region: oc.Region, + Zone: oc.Zone, + BaseURL: DefaultBaseURL, + Timeout: DefaultTimeoutSec, + Active: true, + PrivateKey: oc.PrivateKey, + PublicKey: oc.PublicKey, + MaxRetryTimes: sdk.Int(DefaultMaxRetryTimes), + } + err = os.Rename(ConfigFilePath, ConfigFilePath+".old") + if err != nil { + return err + } + return AggConfigListIns.Append(ac) +} + +// GetBizClient 初始化BizClient +func GetBizClient(ac *AggConfig) (*Client, error) { + timeout, err := time.ParseDuration(fmt.Sprintf("%ds", ac.Timeout)) + if err != nil { + err = fmt.Errorf("parse timeout %ds failed: %v", ac.Timeout, err) + } ClientConfig = &sdk.Config{ - ProjectId: ConfigInstance.ProjectID, - BaseUrl: "https://api.ucloud.cn/", - Timeout: timeout, - UserAgent: fmt.Sprintf("UCloud CLI v%s", Version), - LogLevel: 1, + BaseUrl: ac.BaseURL, + Timeout: timeout, + UserAgent: UserAgent, + LogLevel: log.FatalLevel, + Region: ac.Region, + ProjectId: ac.ProjectID, + MaxRetries: *ac.MaxRetryTimes, + } + AuthCredential = &CredentialConfig{ + PublicKey: ac.PublicKey, + PrivateKey: ac.PrivateKey, + Cookie: ac.Cookie, + CSRFToken: ac.CSRFToken, + } + return NewClient(ClientConfig, AuthCredential), err +} + +func InitConfigInCloudShell() error { + configFile, err := os.OpenFile(ConfigFilePath, os.O_CREATE|os.O_RDONLY, LocalFileMode) + if err != nil { + return err + } + credFile, err := os.OpenFile(CredentialFilePath, os.O_CREATE|os.O_RDONLY, LocalFileMode) + if err != nil { + return err + } + + data, err := ioutil.ReadAll(credFile) + if err != nil { + return err + } + if len(data) > 0 { + var credConfigs []CredentialConfig + err = json.Unmarshal(data, &credConfigs) + if err != nil { + return err + } + if len(credConfigs) > 0 { + cred := credConfigs[0] + if cred.Cookie != "" && cred.CSRFToken != "" { + return nil + } + } + } + + AggConfigM, err := NewInCloudShell() + if err != nil { + return err + } + + AggConfigM.credFile = credFile + AggConfigM.configFile = configFile + ins, err := AggConfigM.GetActiveAggConfig() + if err != nil { + return err + } + ConfigIns = ins + bc, err := GetBizClient(ConfigIns) + if err != nil { + return err + } + BizClient = bc + return AggConfigM.Save() +} + +// InitConfig 初始化配置 +func InitConfig() { + configFile, err := os.OpenFile(ConfigFilePath, os.O_CREATE|os.O_RDONLY, LocalFileMode) + if err != nil && !os.IsNotExist(err) { + HandleError(err) + } + credFile, err := os.OpenFile(CredentialFilePath, os.O_CREATE|os.O_RDONLY, LocalFileMode) + if err != nil && !os.IsNotExist(err) { + HandleError(err) + } + + AggConfigListIns, err = NewAggConfigManager(configFile, credFile) + if err != nil { + LogError(err.Error()) + return + } + + var ins *AggConfig + if Global.Profile == "" { + ins, err = AggConfigListIns.GetActiveAggConfig() + if err != nil && len(AggConfigListIns.GetAggConfigList()) != 0 { + HandleError(err) + } + } else { + ins, _ = AggConfigListIns.GetAggConfigByProfile(Global.Profile) } - Credential = &auth.Credential{ - PublicKey: ConfigInstance.PublicKey, - PrivateKey: ConfigInstance.PrivateKey, + if ins != nil { + ConfigIns = ins } - //sdkClient 用于上报数据 - SdkClient = sdk.NewClient(ClientConfig, Credential) + mergeConfigIns(ConfigIns) + logCmd() - //bizClient 用于调用业务接口 - BizClient = NewClient(ClientConfig, Credential) + bc, err := GetBizClient(ConfigIns) + if err != nil { + HandleError(err) + } else { + BizClient = bc + } +} + +func mergeConfigIns(ins *AggConfig) { + if Global.BaseURL != "" { + ins.BaseURL = Global.BaseURL + } + if Global.Timeout != 0 { + ins.Timeout = Global.Timeout + } + if Global.MaxRetryTimes != -1 { + ins.MaxRetryTimes = sdk.Int(Global.MaxRetryTimes) + } + + if Global.PublicKey != "" && Global.PrivateKey != "" { + ins.PrivateKey = Global.PrivateKey + ins.PublicKey = Global.PublicKey + } +} + +func init() { + //配置日志 + err := initLog() + if err != nil { + fmt.Println(err) + } } diff --git a/base/config_test.go b/base/config_test.go new file mode 100644 index 0000000000..1bb244ee8b --- /dev/null +++ b/base/config_test.go @@ -0,0 +1,89 @@ +package base + +import ( + "io/ioutil" + "os" + "testing" +) + +const cliConfigJSON = `[ + {"project_id":"org-bdks4e","region":"cn-bj2","zone":"cn-bj2-04","base_url":"https://api.ucloud.cn/","timeout_sec":15,"profile":"uweb","active":true}, + {"project_id":"org-oxjwoi","region":"hk","zone":"hk-02","base_url":"https://api.ucloud.cn/","timeout_sec":15,"profile":"test","active":false} +]` + +const credentialJSON = `[ + {"public_key":"4E9UU*****3ZAPWQ==","private_key":"6945*****a0d45","profile":"uweb"}, + {"public_key":"YSQG*****zgnCRQ=","private_key":"jtma*****Avms","profile":"test"} +]` + +func TestAggConfigManager(t *testing.T) { + os.MkdirAll(".ucloud", 0700) + err := ioutil.WriteFile(".ucloud/config.json", []byte(cliConfigJSON), LocalFileMode) + if err != nil { + t.Error(err) + } + err = ioutil.WriteFile(".ucloud/credential.json", []byte(credentialJSON), LocalFileMode) + if err != nil { + t.Error(err) + } + defer func() { + err := os.RemoveAll(".ucloud") + if err != nil { + t.Error(err) + } + }() + + configFile, err := os.OpenFile(".ucloud/config.json", os.O_CREATE|os.O_RDONLY, LocalFileMode) + if err != nil { + t.Error(err) + } + + credFile, err := os.OpenFile(".ucloud/credential.json", os.O_CREATE|os.O_RDONLY, LocalFileMode) + if err != nil { + t.Error(err) + } + + acManager, err := NewAggConfigManager(configFile, credFile) + if err != nil { + t.Error(err) + } + + if len(acManager.configs) != 2 { + t.Errorf("expect length of configs is 2, accpet %d", len(acManager.configs)) + } + +} + +func TestEmptyAggConfigManager(t *testing.T) { + os.MkdirAll(".ucloud", 0700) + defer func() { + err := os.RemoveAll(".ucloud") + if err != nil { + t.Error(err) + } + }() + + configFile, err := os.OpenFile(".ucloud/config.json", os.O_CREATE|os.O_RDONLY, LocalFileMode) + if err != nil { + t.Error(err) + } + + credFile, err := os.OpenFile(".ucloud/credential.json", os.O_CREATE|os.O_RDONLY, LocalFileMode) + if err != nil { + t.Error(err) + } + + acManager, err := NewAggConfigManager(configFile, credFile) + if err != nil { + t.Error(err) + } + + err = acManager.Load() + if err != nil { + t.Fatal(err) + } + + if len(acManager.configs) != 0 { + t.Errorf("expect length of configs is 2, accpet %d", len(acManager.configs)) + } +} diff --git a/base/log.go b/base/log.go new file mode 100644 index 0000000000..da036404df --- /dev/null +++ b/base/log.go @@ -0,0 +1,294 @@ +package base + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "os" + "runtime" + "strings" + "sync" + "time" + + uuid "github.com/satori/go.uuid" + log "github.com/sirupsen/logrus" + + "github.com/ucloud/ucloud-sdk-go/ucloud/request" + "github.com/ucloud/ucloud-sdk-go/ucloud/version" +) + +const DefaultDasURL = "https://das-rpt.ucloud.cn/log" + +// Logger 日志 +var logger *log.Logger +var mu sync.Mutex +var out = Cxt.GetWriter() +var tracer = Tracer{DefaultDasURL} + +func initConfigDir() { + if _, err := os.Stat(GetLogFileDir()); os.IsNotExist(err) { + err := os.MkdirAll(GetLogFileDir(), LocalFileMode) + if err != nil { + panic(err) + } + } +} + +func initLog() error { + initConfigDir() + file, err := os.OpenFile(GetLogFilePath(), os.O_APPEND|os.O_CREATE|os.O_RDWR, 0644) + if err != nil { + return fmt.Errorf("open log file failed: %v", err) + } + logger = log.New() + logger.SetNoLock() + logger.AddHook(NewLogRotateHook(file)) + logger.SetOutput(file) + + return nil +} + +func logCmd() { + args := make([]string, len(os.Args)) + copy(args, os.Args) + for idx, arg := range args { + for _, word := range []string{"password", "private-key", "public-key"} { + if strings.Contains(arg, word) && idx <= len(args)-2 { + args[idx+1] = strings.Repeat("*", 8) + } + } + } + LogInfo(fmt.Sprintf("command: %s", strings.Join(args, " "))) +} + +// GetLogger return point of logger +func GetLogger() *log.Logger { + return logger +} + +// GetLogFileDir 获取日志文件路径 +func GetLogFileDir() string { + return GetHomePath() + fmt.Sprintf("/%s", ConfigPath) +} + +// GetLogFilePath 获取日志文件路径 +func GetLogFilePath() string { + return GetHomePath() + fmt.Sprintf("/%s/cli.log", ConfigPath) +} + +// LogInfo 记录日志 +func LogInfo(logs ...string) { + _, ok := os.LookupEnv("COMP_LINE") + if ok { + return + } + mu.Lock() + defer mu.Unlock() + goID := curGoroutineID() + for _, line := range logs { + logger.WithField("goroutine_id", goID).Info(line) + } + if ConfigIns.AgreeUploadLog { + UploadLogs(logs, "info", goID) + } +} + +// LogPrint 记录日志 +func LogPrint(logs ...string) { + _, ok := os.LookupEnv("COMP_LINE") + if ok { + return + } + mu.Lock() + defer mu.Unlock() + goID := curGoroutineID() + for _, line := range logs { + logger.WithField("goroutine_id", goID).Print(line) + fmt.Fprintln(out, line) + } + if ConfigIns.AgreeUploadLog { + UploadLogs(logs, "print", goID) + } +} + +// LogWarn 记录日志 +func LogWarn(logs ...string) { + _, ok := os.LookupEnv("COMP_LINE") + if ok { + return + } + mu.Lock() + defer mu.Unlock() + goID := curGoroutineID() + for _, line := range logs { + logger.WithField("goroutine_id", goID).Warn(line) + fmt.Fprintln(out, line) + } + if ConfigIns.AgreeUploadLog { + UploadLogs(logs, "warn", goID) + } +} + +// LogError 记录日志 +func LogError(logs ...string) { + _, ok := os.LookupEnv("COMP_LINE") + if ok { + return + } + mu.Lock() + defer mu.Unlock() + goID := curGoroutineID() + for _, line := range logs { + logger.WithField("goroutine_id", goID).Error(line) + fmt.Fprintln(out, line) + } + if ConfigIns.AgreeUploadLog { + UploadLogs(logs, "error", goID) + } +} + +// UploadLogs send logs to das server +func UploadLogs(logs []string, level string, goID int64) { + var lines []string + for _, log := range logs { + line := fmt.Sprintf("time=%s level=%s goroutine_id=%d msg=%s", time.Now().Format(time.RFC3339Nano), level, goID, log) + lines = append(lines, line) + } + tracer.Send(lines) +} + +// LogRotateHook rotate log file +type LogRotateHook struct { + MaxSize int64 + Cut float32 + LogFile *os.File + mux sync.Mutex +} + +// Levels fires hook +func (hook *LogRotateHook) Levels() []log.Level { + return log.AllLevels +} + +// Fire do someting when hook is triggered +func (hook *LogRotateHook) Fire(entry *log.Entry) error { + hook.mux.Lock() + defer hook.mux.Unlock() + info, err := hook.LogFile.Stat() + if err != nil { + return err + } + + if info.Size() <= hook.MaxSize { + return nil + } + hook.LogFile.Sync() + offset := int64(float32(hook.MaxSize) * hook.Cut) + buf := make([]byte, info.Size()-offset) + _, err = hook.LogFile.ReadAt(buf, offset) + if err != nil { + return err + } + + nfile, err := os.Create(GetLogFilePath() + ".tmp") + if err != nil { + return err + } + nfile.Write(buf) + nfile.Close() + + err = os.Rename(GetLogFilePath()+".tmp", GetLogFilePath()) + if err != nil { + return err + } + + mfile, err := os.OpenFile(GetLogFilePath(), os.O_APPEND|os.O_CREATE|os.O_RDWR, 0644) + if err != nil { + fmt.Println("open log file failed: ", err) + return err + } + entry.Logger.SetOutput(mfile) + return nil +} + +// NewLogRotateHook create a LogRotateHook +func NewLogRotateHook(file *os.File) *LogRotateHook { + return &LogRotateHook{ + MaxSize: 1024 * 1024, //1MB + Cut: 0.2, + LogFile: file, + } +} + +// ToQueryMap tranform request to map +func ToQueryMap(req request.Common) map[string]string { + reqMap, err := request.ToQueryMap(req) + if err != nil { + return nil + } + delete(reqMap, "Password") + return reqMap +} + +// Tracer upload log to server if allowed +type Tracer struct { + DasUrl string +} + +func (t Tracer) wrapLogs(log []string) ([]byte, error) { + dataSet := make([]map[string]interface{}, 0) + dataItem := map[string]interface{}{ + "level": "info", + "topic": "api", + "log": log, + } + dataSet = append(dataSet, dataItem) + reqUUID := uuid.NewV4() + sessionID := uuid.NewV4() + user, err := GetUserInfo() + if err != nil { + return nil, err + } + payload := map[string]interface{}{ + "aid": "iywtleaa", + "uuid": reqUUID, + "sid": sessionID, + "ds": dataSet, + "cs": map[string]interface{}{ + "uname": user.UserEmail, + }, + } + marshaled, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("cannot to marshal log: %s", err) + } + return marshaled, nil +} + +// Send logs to server +func (t Tracer) Send(logs []string) error { + body, err := t.wrapLogs(logs) + if err != nil { + return err + } + for i := 0; i < len(body); i++ { + body[i] = ^body[i] + } + + client := &http.Client{} + ua := fmt.Sprintf("GO/%s GO-SDK/%s %s", runtime.Version(), version.Version, UserAgent) + req, err := http.NewRequest("POST", t.DasUrl, bytes.NewReader(body)) + req.Header.Add("Origin", "https://sdk.ucloud.cn") + req.Header.Add("User-Agent", ua) + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return fmt.Errorf("send logs failed: status %d %s", resp.StatusCode, resp.Status) + } + + return nil +} diff --git a/base/util.go b/base/util.go index 3539fdb9ab..a49f75878b 100644 --- a/base/util.go +++ b/base/util.go @@ -2,9 +2,11 @@ package base import ( "bufio" + "encoding/base64" "encoding/json" "fmt" "io" + "io/ioutil" "os" "reflect" "runtime" @@ -17,27 +19,26 @@ import ( uerr "github.com/ucloud/ucloud-sdk-go/ucloud/error" "github.com/ucloud/ucloud-sdk-go/ucloud/helpers/waiter" "github.com/ucloud/ucloud-sdk-go/ucloud/log" + "github.com/ucloud/ucloud-sdk-go/ucloud/request" "github.com/ucloud/ucloud-sdk-go/ucloud/response" "github.com/ucloud/ucloud-cli/model" + "github.com/ucloud/ucloud-cli/ux" ) -//ConfigPath 配置文件路径 +// ConfigPath 配置文件路径 const ConfigPath = ".ucloud" -//GAP 表格列直接的间隔字符数 +// GAP 表格列直接的间隔字符数 const GAP = 2 -//Cxt 上下文 +// Cxt 上下文 var Cxt = model.GetContext(os.Stdout) -//SdkClient 用于上报数据 +// SdkClient 用于上报数据 var SdkClient *sdk.Client -//BizClient 用于调用业务接口 -var BizClient *Client - -//GetHomePath 获取家目录 +// GetHomePath 获取家目录 func GetHomePath() string { if runtime.GOOS == "windows" { home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") @@ -49,16 +50,16 @@ func GetHomePath() string { return os.Getenv("HOME") } -//MosaicString 对字符串敏感部分打马赛克 如公钥私钥 -func MosaicString(s string, beginChars, lastChars int) string { - r := len(s) - lastChars - beginChars - if r > 0 { - return s[:beginChars] + strings.Repeat("*", r) + s[(r+beginChars):] +// MosaicString 对字符串敏感部分打马赛克 如公钥私钥 +func MosaicString(str string, beginChars, lastChars int) string { + r := len(str) - lastChars - beginChars + if r > 5 { + return str[:beginChars] + strings.Repeat("*", 5) + str[(r+beginChars):] } - return strings.Repeat("*", len(s)) + return strings.Repeat("*", len(str)) } -//AppendToFile 添加到文件中 +// AppendToFile 添加到文件中 func AppendToFile(name string, content string) error { f, err := os.OpenFile(name, os.O_RDWR|os.O_APPEND, 0) if err != nil { @@ -69,7 +70,7 @@ func AppendToFile(name string, content string) error { return err } -//LineInFile 检查某一行是否在某文件中 +// LineInFile 检查某一行是否在某文件中 func LineInFile(fileName string, lookFor string) bool { f, err := os.Open(fileName) if err != nil { @@ -98,8 +99,8 @@ func LineInFile(fileName string, lookFor string) bool { } } -//GetConfigPath 获取配置文件的绝对路径 -func GetConfigPath() string { +// GetConfigDir 获取配置文件所在目录 +func GetConfigDir() string { path := GetHomePath() + "/" + ConfigPath if _, err := os.Stat(path); os.IsNotExist(err) { err = os.MkdirAll(path, 0755) @@ -110,34 +111,50 @@ func GetConfigPath() string { return path } -//HandleBizError 处理RetCode != 0 的业务异常 +// HandleBizError 处理RetCode != 0 的业务异常 func HandleBizError(resp response.Common) error { format := "Something wrong. RetCode:%d. Message:%s\n" - Cxt.Printf(format, resp.GetRetCode(), resp.GetMessage()) + LogError(fmt.Sprintf(format, resp.GetRetCode(), resp.GetMessage())) return fmt.Errorf(format, resp.GetRetCode(), resp.GetMessage()) } -//HandleError 处理错误,业务错误 和 HTTP错误 +// HandleError 处理错误,业务错误 和 HTTP错误 func HandleError(err error) { if uErr, ok := err.(uerr.Error); ok && uErr.Code() != 0 { format := "Something wrong. RetCode:%d. Message:%s\n" - Cxt.Printf(format, uErr.Code(), uErr.Message()) + LogError(fmt.Sprintf(format, uErr.Code(), uErr.Message())) } else { - Cxt.PrintErr(err) + LogError(fmt.Sprintf("%v", err)) + } +} + +// ParseError 解析错误为字符串 +func ParseError(err error) string { + if uErr, ok := err.(uerr.Error); ok && uErr.Code() != 0 { + format := "Something wrong. RetCode:%d. Message:%s" + message := uErr.Message() + if uErr.Code() == -1 || uErr.Code() == -2 { + message = "request timeout, retry later please" + } + return fmt.Sprintf(format, uErr.Code(), message) } + return fmt.Sprintf("Error:%v", err) } -//PrintJSON 以JSON格式打印数据集合 -func PrintJSON(dataSet interface{}) error { +// PrintJSON 以JSON格式打印数据集合 +func PrintJSON(dataSet interface{}, out io.Writer) error { bytes, err := json.MarshalIndent(dataSet, "", " ") if err != nil { return err } - Cxt.Println(string(bytes)) + _, err = fmt.Fprintln(out, string(bytes)) + if err != nil { + return err + } return nil } -//PrintTableS 简化版表格打印,无需传表头,根据结构体反射解析 +// PrintTableS 简化版表格打印,无需传表头,根据结构体反射解析 func PrintTableS(dataSet interface{}) { dataSetVal := reflect.ValueOf(dataSet) fieldNameList := make([]string, 0) @@ -154,7 +171,29 @@ func PrintTableS(dataSet interface{}) { } } -//PrintTable 以表格方式打印数据集合 +// PrintList 打印表格或者JSON +func PrintList(dataSet interface{}, out io.Writer) { + if Global.JSON { + PrintJSON(dataSet, out) + } else { + PrintTableS(dataSet) + } +} + +// PrintDescribe 打印详情 +func PrintDescribe(attrs []DescribeTableRow, json bool) { + if json { + PrintJSON(attrs, os.Stdout) + } else { + for _, attr := range attrs { + fmt.Println(attr.Attribute) + fmt.Println(attr.Content) + fmt.Println() + } + } +} + +// PrintTable 以表格方式打印数据集合 func PrintTable(dataSet interface{}, fieldList []string) { dataSetVal := reflect.ValueOf(dataSet) switch dataSetVal.Kind() { @@ -174,41 +213,64 @@ func displaySlice(listVal reflect.Value, fieldList []string) { for i := 0; i < listVal.Len(); i++ { elemVal := listVal.Index(i) elemType := elemVal.Type() - row := make(map[string]interface{}) + var rows []map[string]interface{} for j := 0; j < elemVal.NumField(); j++ { field := elemVal.Field(j) fieldName := elemType.Field(j).Name if _, ok := showFieldMap[fieldName]; ok { - row[fieldName] = field.Interface() + if field.Kind() == reflect.Ptr { + field = field.Elem() + } text := fmt.Sprintf("%v", field.Interface()) - width := calcWidth(text) - if showFieldMap[fieldName] < width { - showFieldMap[fieldName] = width + cells := strings.Split(text, "\n") + for i, cell := range cells { + width := calcWidth(cell) + if showFieldMap[fieldName] < width { + showFieldMap[fieldName] = width + } + if len(rows) == i { + rows = append(rows, make(map[string]interface{})) + } + rows[i][fieldName] = cell } } } - rowList = append(rowList, row) + rowList = append(rowList, rows...) } printTable(rowList, fieldList, showFieldMap) } func printTable(rowList []map[string]interface{}, fieldList []string, fieldWidthMap map[string]int) { + //打印表头 for _, field := range fieldList { tmpl := "%-" + strconv.Itoa(fieldWidthMap[field]+GAP) + "s" fmt.Printf(tmpl, field) } - fmt.Printf("\n") + if len(fieldList) != 0 { + fmt.Printf("\n") + } + //打印数据 for _, row := range rowList { for _, field := range fieldList { cutWidth := calcCutWidth(fmt.Sprintf("%v", row[field])) tmpl := "%-" + strconv.Itoa(fieldWidthMap[field]-cutWidth+GAP) + "v" - fmt.Printf(tmpl, row[field]) + if row[field] != nil { + fmt.Printf(tmpl, row[field]) + } else { + fmt.Printf(tmpl, "") + } } fmt.Printf("\n") } } +// DescribeTableRow 详情表格通用表格行 +type DescribeTableRow struct { + Attribute string + Content string +} + func calcCutWidth(text string) int { set := []*unicode.RangeTable{unicode.Han, unicode.Punct} width := 0 @@ -233,17 +295,26 @@ func calcWidth(text string) int { return width } -//FormatDate 格式化时间,把以秒为单位的时间戳格式化未年月日 +// FormatDate 格式化时间,把以秒为单位的时间戳格式化未年月日 func FormatDate(seconds int) string { return time.Unix(int64(seconds), 0).Format("2006-01-02") } -//RegionLabel regionlable +// DateTimeLayout 时间格式 +const DateTimeLayout = "2006-01-02/15:04:05" + +// FormatDateTime 格式化时间,把以秒为单位的时间戳格式化未年月日/时分秒 +func FormatDateTime(seconds int) string { + return time.Unix(int64(seconds), 0).Format("2006-01-02/15:04:05") +} + +// RegionLabel regionlable var RegionLabel = map[string]string{ "cn-bj1": "Beijing1", "cn-bj2": "Beijing2", "cn-sh2": "Shanghai2", "cn-gd": "Guangzhou", + "cn-qz": "Quanzhou", "hk": "Hongkong", "us-ca": "LosAngeles", "us-ws": "Washington", @@ -263,66 +334,376 @@ var RegionLabel = map[string]string{ "afr-nigeria": "Lagos", } -//Poll 轮询 -func Poll(describeFunc func(string, string, string, string) (interface{}, error)) func(string, string, string, string, []string) chan bool { - stateFields := []string{"State", "Status"} - return func(resourceID, projectID, region, zone string, targetState []string) chan bool { - w := waiter.StateWaiter{ - Pending: []string{"pending"}, - Target: []string{"avaliable"}, - Refresh: func() (interface{}, string, error) { - inst, err := describeFunc(resourceID, projectID, region, zone) - if err != nil { - return nil, "", err - } +// Poller 轮询器 +type Poller struct { + stateFields []string + DescribeFunc func(string, string, string, string) (interface{}, error) + Out io.Writer + Timeout time.Duration + SdescribeFunc func(string, *request.CommonBase) (interface{}, error) + SdescribeWithCommonConfigFunc func(string) (interface{}, error) +} + +type pollResult struct { + Done bool + Timeout bool + Err error +} - if inst == nil { - return nil, "pending", nil +// Sspoll 简化版, 支持并发 +func (p *Poller) Sspoll(resourceID, pollText string, targetStates []string, block *ux.Block, commonBase *request.CommonBase) *pollResult { + w := waiter.StateWaiter{ + Pending: []string{"pending"}, + Target: []string{"avaliable"}, + Refresh: func() (interface{}, string, error) { + inst, err := p.SdescribeFunc(resourceID, commonBase) + if err != nil { + return nil, "", err + } + + if inst == nil { + return nil, "pending", nil + } + instValue := reflect.ValueOf(inst) + instValue = reflect.Indirect(instValue) + instType := instValue.Type() + if instValue.Kind() != reflect.Struct { + return nil, "", fmt.Errorf("Instance is not struct") + } + state := "" + for i := 0; i < instValue.NumField(); i++ { + for _, sf := range p.stateFields { + if instType.Field(i).Name == sf { + state = instValue.Field(i).String() + } } - instValue := reflect.ValueOf(inst) - instValue = reflect.Indirect(instValue) - instType := instValue.Type() - if instValue.Kind() != reflect.Struct { - return nil, "", fmt.Errorf("Instance is not struct") + } + if state != "" { + for _, t := range targetStates { + if t == state { + return inst, "avaliable", nil + } } - state := "" - for i := 0; i < instValue.NumField(); i++ { - for _, sf := range stateFields { - if instType.Field(i).Name == sf { - state = instValue.Field(i).String() - } + } + return nil, "pending", nil + + }, + Timeout: p.Timeout, + } + + pollRetChan := make(chan pollResult) + go func() { + ret := pollResult{ + Done: true, + } + if _, err := w.Wait(); err != nil { + ret.Done = false + ret.Err = err + if _, ok := err.(*waiter.TimeoutError); ok { + ret.Timeout = true + } + } + pollRetChan <- ret + }() + + spin := ux.NewDotSpin(p.Out, pollText) + block.SetSpin(spin) + + ret := <-pollRetChan + + if ret.Timeout { + spin.Timeout() + } else { + spin.Stop() + } + return &ret +} + +// Spoll 简化版 +func (p *Poller) Spoll(resourceID, pollText string, targetStates []string) { + w := waiter.StateWaiter{ + Pending: []string{"pending"}, + Target: []string{"avaliable"}, + Refresh: func() (interface{}, string, error) { + inst, err := p.SdescribeFunc(resourceID, nil) + if err != nil { + return nil, "", err + } + + if inst == nil { + return nil, "pending", nil + } + instValue := reflect.ValueOf(inst) + instValue = reflect.Indirect(instValue) + instType := instValue.Type() + if instValue.Kind() != reflect.Struct { + return nil, "", fmt.Errorf("Instance is not struct") + } + state := "" + for i := 0; i < instValue.NumField(); i++ { + for _, sf := range p.stateFields { + if instType.Field(i).Name == sf { + state = instValue.Field(i).String() } } - if state != "" { - for _, t := range targetState { - if t == state { - return inst, "avaliable", nil - } + } + if state != "" { + for _, t := range targetStates { + if t == state { + return inst, "avaliable", nil } } - return nil, "pending", nil + } + return nil, "pending", nil + + }, + Timeout: p.Timeout, + } - }, - Timeout: 5 * time.Minute, + done := make(chan bool) + go func() { + if _, err := w.Wait(); err != nil { + log.Error(err) + if _, ok := err.(*waiter.TimeoutError); ok { + done <- false + return + } } + done <- true + }() - done := make(chan bool) - go func() { - if resp, err := w.Wait(); err != nil { - log.Error(err) - } else { - log.Infof("%#v", resp) + spinner := ux.NewDotSpinner(p.Out) + spinner.Start(pollText) + ret := <-done + if ret { + spinner.Stop() + } else { + spinner.Timeout() + } +} + +// Poll function +func (p *Poller) Poll(resourceID, projectID, region, zone, pollText string, targetState []string) bool { + w := waiter.StateWaiter{ + Pending: []string{"pending"}, + Target: []string{"avaliable"}, + Refresh: func() (interface{}, string, error) { + inst, err := p.DescribeFunc(resourceID, projectID, region, zone) + if err != nil { + return nil, "", err + } + + if inst == nil { + return nil, "pending", nil } - done <- true - }() - return done + instValue := reflect.ValueOf(inst) + instValue = reflect.Indirect(instValue) + instType := instValue.Type() + if instValue.Kind() != reflect.Struct { + return nil, "", fmt.Errorf("Instance is not struct") + } + state := "" + for i := 0; i < instValue.NumField(); i++ { + for _, sf := range p.stateFields { + if instType.Field(i).Name == sf { + state = instValue.Field(i).String() + } + } + } + if state != "" { + for _, t := range targetState { + if t == state { + return inst, "avaliable", nil + } + } + } + return nil, "pending", nil + + }, + Timeout: p.Timeout, + } + + var err error + done := make(chan bool) + go func() { + if _, err = w.Wait(); err != nil { + done <- false + return + } + done <- true + }() + + spinner := ux.NewDotSpinner(p.Out) + spinner.Start(pollText) + ret := <-done + if err != nil { + spinner.Fail(err) + } else { + spinner.Stop() + } + return ret +} + +// NewSpoller simple +func NewSpoller(describeFunc func(string, *request.CommonBase) (interface{}, error), out io.Writer) *Poller { + return &Poller{ + SdescribeFunc: describeFunc, + Out: out, + stateFields: []string{"State", "Status"}, + Timeout: 10 * time.Minute, + } +} + +// NewPoller 轮询 +func NewPoller(describeFunc func(string, string, string, string) (interface{}, error), out io.Writer) *Poller { + return &Poller{ + DescribeFunc: describeFunc, + Out: out, + stateFields: []string{"State", "Status"}, + Timeout: 10 * time.Minute, } } -//PickResourceID uhost-xxx/uhost-name => uhost-xxx +// PickResourceID uhost-xxx/uhost-name => uhost-xxx func PickResourceID(str string) string { if strings.Index(str, "/") > -1 { return strings.SplitN(str, "/", 2)[0] } return str } + +// WriteJSONFile 写json文件 +func WriteJSONFile(list interface{}, filePath string) error { + byts, err := json.Marshal(list) + if err != nil { + return err + } + err = ioutil.WriteFile(filePath, byts, 0600) + if err != nil { + return err + } + return nil +} + +// GetFileList 补全文件名 +func GetFileList(suffix string) []string { + cmdLine := strings.TrimSpace(os.Getenv("COMP_LINE")) + words := strings.Split(cmdLine, " ") + last := words[len(words)-1] + pathPrefix := "." + + if !strings.HasPrefix(last, "-") { + pathPrefix = last + } + hasTilde := false + //https://tiswww.case.edu/php/chet/bash/bashref.html#Tilde-Expansion + if strings.HasPrefix(pathPrefix, "~") { + pathPrefix = strings.Replace(pathPrefix, "~", GetHomePath(), 1) + hasTilde = true + } + files, err := ioutil.ReadDir(pathPrefix) + if err != nil { + return nil + } + names := []string{} + for _, f := range files { + name := f.Name() + if !strings.HasSuffix(name, suffix) { + continue + } + if hasTilde { + pathPrefix = strings.Replace(pathPrefix, GetHomePath(), "~", 1) + } + if strings.HasSuffix(pathPrefix, "/") { + names = append(names, pathPrefix+name) + } else { + names = append(names, pathPrefix+"/"+name) + } + } + return names +} + +// Confirm 二次确认 +func Confirm(yes bool, text string) bool { + if yes { + return true + } + sure, err := ux.Prompt(text) + if err != nil { + LogError(err.Error()) + return false + } + return sure +} + +func curGoroutineID() int64 { + var ( + buf [64]byte + n = runtime.Stack(buf[:], false) + stk = strings.TrimPrefix(string(buf[:n]), "goroutine ") + ) + + idField := strings.Fields(stk)[0] + id, err := strconv.Atoi(idField) + if err != nil { + panic(fmt.Errorf("can not get goroutine id: %v", err)) + } + + return int64(id) +} + +func getDefaultRegion(cookie, csrfToken string) (string, string, error) { + cfg := &AggConfig{ + Cookie: cookie, + BaseURL: DefaultBaseURL, + CSRFToken: csrfToken, + Timeout: DefaultTimeoutSec, + MaxRetryTimes: sdk.Int(DefaultMaxRetryTimes), + } + bc, err := GetBizClient(cfg) + req := bc.NewGetRegionRequest() + if err != nil { + return "", "", err + } + resp, err := bc.GetRegion(req) + if err != nil { + return "", "", err + } + for _, r := range resp.Regions { + if r.IsDefault { + return r.Region, r.Zone, nil + } + } + return "", "", fmt.Errorf("default region not found") +} + +func getDefaultProject(cookie, csrfToken string) (string, string, error) { + cfg := &AggConfig{ + Cookie: cookie, + BaseURL: DefaultBaseURL, + CSRFToken: csrfToken, + Timeout: DefaultTimeoutSec, + MaxRetryTimes: sdk.Int(DefaultMaxRetryTimes), + } + bc, err := GetBizClient(cfg) + if err != nil { + return "", "", err + } + + req := bc.NewGetProjectListRequest() + resp, err := bc.GetProjectList(req) + if err != nil { + return "", "", err + } + for _, project := range resp.ProjectSet { + if project.IsDefault == true { + return project.ProjectId, project.ProjectName, nil + } + } + return "", "", fmt.Errorf("default project not found") +} + +func IsBase64Encoded(data []byte) bool { + _, err := base64.StdEncoding.DecodeString(string(data)) + return err == nil +} diff --git a/cmd/api.go b/cmd/api.go new file mode 100644 index 0000000000..10441ad347 --- /dev/null +++ b/cmd/api.go @@ -0,0 +1,261 @@ +package cmd + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/spf13/cobra" + "github.com/ucloud/ucloud-sdk-go/ucloud" + "github.com/ucloud/ucloud-sdk-go/ucloud/request" + + "github.com/ucloud/ucloud-cli/base" + "github.com/ucloud/ucloud-cli/model/status" + "github.com/ucloud/ucloud-cli/ux" +) + +type RepeatsConfig struct { + Poller *base.Poller + IDInResp string +} + +var RepeatsSupportedAPI = map[string]RepeatsConfig{ + "CreateULHostInstance": {Poller: ulhostSpoller, IDInResp: "ULHostId"}, +} + +const ActionField = "Action" +const RepeatsField = "repeats" +const ConcurrentField = "concurrent" +const DefaultConcurrent = 20 +const HelpField = "help" +const HelpInfo = `Usage: ucloud api [options] --Action actionName --param1 value1 --param2 value2 ... +Options: + --local-file string the path of the local file which contains the api parameters + --repeats string the number of repeats + --concurrent string the number of concurrent + --help show help` + +// NewCmdAPI ucloud api --xkey xvalue +func NewCmdAPI(out io.Writer) *cobra.Command { + return &cobra.Command{ + Use: "api", + Short: "Call API", + Long: "Call API", + Run: func(c *cobra.Command, args []string) { + if containHelp(args) { + fmt.Fprintln(out, HelpInfo) + return + } + params, err := parseParamsFromCmdLine(args) + if err != nil { + fmt.Fprintln(out, err) + return + } + + if params["local-file"] != nil { + file, ok := params["local-file"].(string) + if !ok { + fmt.Fprintf(out, "local-file should be a string\n") + } + params, err = parseParamsFromJSONFile(file) + if err != nil { + fmt.Fprintln(out, err) + return + } + } + if action, actionOK := params[ActionField].(string); actionOK { + if repeatsConfig, repeatsSupported := RepeatsSupportedAPI[action]; repeatsSupported { + if repeats, repeatsOK := params[RepeatsField].(string); repeatsOK { + var repeatsNum int + var concurrentNum int + repeatsNum, err = strconv.Atoi(repeats) + if err != nil { + fmt.Fprintf(out, "error: %v\n", err) + return + } + if concurrent, concurrentOK := params[ConcurrentField].(string); concurrentOK { + concurrentNum, err = strconv.Atoi(concurrent) + if err != nil { + fmt.Fprintf(out, "error: %v\n", err) + return + } + } else { + concurrentNum = DefaultConcurrent + } + delete(params, RepeatsField) + delete(params, ConcurrentField) + err = genericInvokeRepeatWrapper(&repeatsConfig, params, action, repeatsNum, concurrentNum) + if err != nil { + fmt.Fprintf(out, "error: %v\n", err) + return + } + return + } + } + } + req := base.BizClient.UAccountClient.NewGenericRequest() + err = req.SetPayload(params) + if err != nil { + fmt.Fprintf(out, "error: %v\n", err) + return + } + + resp, err := base.BizClient.UAccountClient.GenericInvoke(req) + if err != nil { + fmt.Fprintf(out, "error: %v\n", err) + return + } + + data, err := json.MarshalIndent(resp.GetPayload(), "", " ") + if err != nil { + fmt.Fprintf(out, "error: %v\n", err) + return + } + fmt.Fprintln(out, string(data)) + }, + } +} + +func parseParamsFromJSONFile(path string) (map[string]interface{}, error) { + content, err := ioutil.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read file error: %w", err) + } + params := make(map[string]interface{}) + err = json.Unmarshal(content, ¶ms) + if err != nil { + return nil, fmt.Errorf("parse json error: %w", err) + } + return params, err +} + +func parseParamsFromCmdLine(args []string) (map[string]interface{}, error) { + if len(args)%2 != 0 { + return nil, errors.New("the key value pairs of api parameters do not match") + } + params := make(map[string]interface{}) + for i := 0; i < len(args)-1; i += 2 { + if strings.HasPrefix(args[i], "--") { + args[i] = args[i][2:] + } + params[args[i]] = args[i+1] + } + return params, nil +} + +func genericInvokeRepeatWrapper(repeatsConfig *RepeatsConfig, params map[string]interface{}, action string, repeats int, concurrent int) error { + if repeatsConfig == nil { + return fmt.Errorf("error: repeatsConfig is nil") + } + if repeats <= 0 { + return fmt.Errorf("error: repeats should be a positive integer") + } + if concurrent <= 0 { + return fmt.Errorf("error: concurrent should be a positive integer") + } + wg := &sync.WaitGroup{} + tokens := make(chan struct{}, concurrent) + retCh := make(chan bool, repeats) + + wg.Add(repeats) + //ux.Doc.Disable() + refresh := ux.NewRefresh() + + req := base.BizClient.UAccountClient.NewGenericRequest() + err := req.SetPayload(params) + if err != nil { + return fmt.Errorf("fail to set payload: %w", err) + } + + go func(req request.GenericRequest) { + for i := 0; i < repeats; i++ { + go func(req request.GenericRequest, idx int) { + tokens <- struct{}{} + defer func() { + <-tokens + //设置延时,使报错能渲染出来 + time.Sleep(time.Second / 5) + wg.Done() + }() + success := true + resp, err := base.BizClient.UAccountClient.GenericInvoke(req) + block := ux.NewBlock() + ux.Doc.Append(block) + logs := []string{"=================================================="} + logs = append(logs, fmt.Sprintf("api:%v, request:%v", action, base.ToQueryMap(req))) + if err != nil { + logs = append(logs, fmt.Sprintf("err:%v", err)) + block.Append(base.ParseError(err)) + success = false + } else { + logs = append(logs, fmt.Sprintf("resp:%#v", resp)) + resourceId, ok := resp.GetPayload()[repeatsConfig.IDInResp].(string) + if !ok { + block.Append(fmt.Sprintf("expect %v in response, but not found", repeatsConfig.IDInResp)) + success = false + } else { + text := fmt.Sprintf("the resource[%s] is initializing", resourceId) + result := repeatsConfig.Poller.Sspoll(resourceId, text, []string{status.HOST_RUNNING, status.HOST_FAIL}, block, &request.CommonBase{ + Region: ucloud.String(req.GetRegion()), + Zone: ucloud.String(req.GetZone()), + ProjectId: ucloud.String(req.GetProjectId()), + }) + if result.Err != nil { + success = false + block.Append(result.Err.Error()) + } + } + retCh <- success + logs = append(logs, fmt.Sprintf("index:%d, result:%t", idx, success)) + base.LogInfo(logs...) + } + }(req, i) + } + }(req) + + var success, fail atomic.Int32 + go func() { + block := ux.NewBlock() + ux.Doc.Append(block) + block.Append(fmt.Sprintf("creating, total:%d, success:%d, fail:%d", repeats, success.Load(), fail.Load())) + blockCount := ux.Doc.GetBlockCount() + for ret := range retCh { + if ret { + success.Add(1) + } else { + fail.Add(1) + } + text := fmt.Sprintf("creating, total:%d, success:%d, fail:%d", repeats, success.Load(), fail.Load()) + if blockCount != ux.Doc.GetBlockCount() { + block = ux.NewBlock() + ux.Doc.Append(block) + block.Append(text) + blockCount = ux.Doc.GetBlockCount() + } else { + block.Update(text, 0) + } + if repeats == int(success.Load())+int(fail.Load()) && fail.Load() > 0 { + fmt.Printf("Check logs in %s\n", base.GetLogFilePath()) + } + } + }() + wg.Wait() + refresh.Do(fmt.Sprintf("finally, total:%d, success:%d, fail:%d", repeats, success.Load(), repeats-int(success.Load()))) + return nil +} + +func containHelp(args []string) bool { + for _, arg := range args { + if arg == "--help" { + return true + } + } + return false +} diff --git a/cmd/backup.go b/cmd/backup.go new file mode 100644 index 0000000000..6c7d4396fc --- /dev/null +++ b/cmd/backup.go @@ -0,0 +1,528 @@ +// Copyright © 2018 NAME HERE tony.li@ucloud.cn +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "io" + "strconv" + "time" + + "github.com/spf13/cobra" + + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + + "github.com/ucloud/ucloud-cli/base" +) + +// NewCmdUDBBackup ucloud udb backup +func NewCmdUDBBackup() *cobra.Command { + cmd := &cobra.Command{ + Use: "backup", + Short: "List and manipulate backups of MySQL instance", + Long: "List and manipulate backups of MySQL instance", + } + out := base.Cxt.GetWriter() + cmd.AddCommand(NewCmdUDBBackupCreate(out)) + cmd.AddCommand(NewCmdUDBBackupList(out)) + cmd.AddCommand(NewCmdUDBBackupDelete(out)) + cmd.AddCommand(NewCmdUDBBackupGetDownloadURL(out)) + return cmd +} + +// NewCmdUDBBackupCreate ucloud udb backup create +func NewCmdUDBBackupCreate(out io.Writer) *cobra.Command { + req := base.BizClient.NewBackupUDBInstanceRequest() + cmd := &cobra.Command{ + Use: "create", + Short: "Create backups for MySQL instance manually", + Long: "Create backups for MySQL instance manually", + Run: func(c *cobra.Command, args []string) { + *req.DBId = base.PickResourceID(*req.DBId) + _, err := base.BizClient.BackupUDBInstance(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintf(out, "udb[%s] backuped\n", *req.DBId) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + req.DBId = flags.String("udb-id", "", "Required. Resource ID of UDB instnace to backup") + req.BackupName = flags.String("name", "", "Required. Name of backup") + bindProjectID(req, flags) + bindRegion(req, flags) + bindZone(req, flags) + + cmd.MarkFlagRequired("udb-id") + cmd.MarkFlagRequired("name") + + flags.SetFlagValuesFunc("udb-id", func() []string { + return getUDBIDList(nil, "sql", *req.ProjectId, *req.Region, *req.Zone) + }) + + return cmd +} + +type udbBackupRow struct { + BackupID int + BackupName string + DB string + BackupSize string + BackupType string + Status string + AvailabilityZone string + BackupBeginTime string + BackupEndTime string +} + +// NewCmdUDBBackupList ucloud udb backup list +func NewCmdUDBBackupList(out io.Writer) *cobra.Command { + var bpType, dbType, beginTime, endTime, backupID string + bpTypeMap := map[string]int{ + "manual": 1, + "auto": 0, + } + reverseBpTypeMap := map[int]string{ + 1: "manual", + 0: "auto", + } + req := base.BizClient.NewDescribeUDBBackupRequest() + cmd := &cobra.Command{ + Use: "list", + Short: "List backups of MySQL instance", + Long: "List backups of MySQL instance", + Run: func(c *cobra.Command, args []string) { + if v, ok := bpTypeMap[bpType]; ok { + req.BackupType = &v + } + if v, ok := dbTypeMap[dbType]; ok { + req.ClassType = &v + } + if *req.DBId != "" { + *req.DBId = base.PickResourceID(*req.DBId) + } + if backupID != "" { + id, err := strconv.Atoi(base.PickResourceID(backupID)) + if err != nil { + base.HandleError(err) + return + } + req.BackupId = &id + } + if beginTime != "" { + bt, err := time.Parse("2006-01-02/15:04:05", beginTime) + if err != nil { + base.HandleError(err) + return + } + req.BeginTime = sdk.Int(int(bt.Unix())) + } + if endTime != "" { + bt, err := time.Parse("2006-01-02/15:04:05", endTime) + if err != nil { + base.HandleError(err) + return + } + req.EndTime = sdk.Int(int(bt.Unix())) + } + resp, err := base.BizClient.DescribeUDBBackup(req) + if err != nil { + base.HandleError(err) + return + } + list := []udbBackupRow{} + for _, ins := range resp.DataSet { + row := udbBackupRow{ + BackupID: ins.BackupId, + BackupName: ins.BackupName, + AvailabilityZone: ins.Zone, + DB: fmt.Sprintf("%s|%s", ins.DBName, ins.DBId), + BackupSize: fmt.Sprintf("%dB", ins.BackupSize), + BackupType: reverseBpTypeMap[ins.BackupType], + Status: ins.State, + BackupBeginTime: base.FormatDateTime(ins.BackupTime), + BackupEndTime: base.FormatDateTime(ins.BackupEndTime), + } + list = append(list, row) + } + base.PrintList(list, out) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + req.DBId = flags.String("udb-id", "", "Optional. Resource ID of UDB for list the backups of the specifid UDB") + flags.StringVar(&backupID, "backup-id", "", "Optional. Resource ID of backup. List the specified backup only") + flags.StringVar(&bpType, "backup-type", "", "Optional. Backup type. Accept values:auto or manual") + flags.StringVar(&dbType, "db-type", "", "Optional. Only list backups of the UDB of the specified DB type") + flags.StringVar(&beginTime, "begin-time", "", "Optional. Begin time of backup. For example, 2019-02-26/11:21:39") + flags.StringVar(&endTime, "end-time", "", "Optional. End time of backup. For example, 2019-02-26/11:31:39") + + bindRegion(req, flags) + bindZone(req, flags) + bindProjectID(req, flags) + bindOffset(req, flags) + bindLimit(req, flags) + + flags.SetFlagValues("backup-type", "auto", "manual") + flags.SetFlagValues("db-type", dbTypeList...) + flags.SetFlagValuesFunc("udb-id", func() []string { + return getUDBIDList(nil, "sql", *req.ProjectId, *req.Region, *req.Zone) + }) + + return cmd +} + +// NewCmdUDBBackupDelete ucloud udb backup delete +func NewCmdUDBBackupDelete(out io.Writer) *cobra.Command { + ids := []int{} + req := base.BizClient.NewDeleteUDBBackupRequest() + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete backups of MySQL instance", + Long: "Delete backups of MySQL instance", + Example: "ucloud udb backup delete --backup-id 65534,65535", + Run: func(c *cobra.Command, args []string) { + for _, id := range ids { + req.BackupId = sdk.Int(id) + _, err := base.BizClient.DeleteUDBBackup(req) + if err != nil { + base.HandleError(err) + continue + } + fmt.Fprintf(out, "backup[%d] deleted\n", id) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + flags.IntSliceVar(&ids, "backup-id", nil, "Required. BackupID of backups to delete") + bindProjectID(req, flags) + bindRegion(req, flags) + bindZone(req, flags) + + cmd.MarkFlagRequired("backup-id") + return cmd +} + +// NewCmdUDBBackupGetDownloadURL ucloud udb backup get-download-url +func NewCmdUDBBackupGetDownloadURL(out io.Writer) *cobra.Command { + req := base.BizClient.NewDescribeUDBInstanceBackupURLRequest() + cmd := &cobra.Command{ + Use: "download", + Short: "Display download url of backup", + Long: "Display download url of backup", + Run: func(c *cobra.Command, args []string) { + resp, err := base.BizClient.DescribeUDBInstanceBackupURL(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintln(out, resp.BackupPath) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + req.BackupId = flags.Int("backup-id", -1, "Required. BackupID of backup to delete") + req.DBId = flags.String("udb-id", "", "Required. Resource ID of udb which the backup belongs to") + bindProjectID(req, flags) + bindRegion(req, flags) + bindZone(req, flags) + + cmd.MarkFlagRequired("udb-id") + cmd.MarkFlagRequired("backup-id") + flags.SetFlagValuesFunc("udb-id", func() []string { + return getUDBIDList(nil, "sql", *req.ProjectId, *req.Region, *req.Zone) + }) + return cmd +} + +// NewCmdUDBLog ucloud udb log +func NewCmdUDBLog() *cobra.Command { + cmd := &cobra.Command{ + Use: "logs", + Short: "List and manipulate logs of MySQL instance", + Long: "List and manipulate logs of MySQL instance", + } + + out := base.Cxt.GetWriter() + cmd.AddCommand(NewCmdUDBLogArchiveCreate(out)) + cmd.AddCommand(NewCmdUDBLogArchiveList(out)) + cmd.AddCommand(NewCmdUDBLogArchiveGetDownloadURL(out)) + cmd.AddCommand(NewCmdUDBLogArchiveDelete(out)) + + return cmd +} + +// NewCmdUDBLogArchiveCreate ucloud udb log archive create +func NewCmdUDBLogArchiveCreate(out io.Writer) *cobra.Command { + var region, zone, project, udbID string + var name, logType, beginTime, endTime string + cmd := &cobra.Command{ + Use: "archive", + Short: "Archive the log of mysql as a compressed file", + Long: "Archive the log of mysql as a compressed file", + Example: "ucloud mysql logs archive --name test.cli2 --udb-id udb-xxx/test.cli1 --log-type slow_query --begin-time 2019-02-23/15:30:00 --end-time 2019-02-24/15:31:00", + Run: func(c *cobra.Command, args []string) { + udbID = base.PickResourceID(udbID) + if logType == "slow_query" { + if beginTime == "" || endTime == "" { + fmt.Fprintln(out, "Error. Both begin-time and end-time can not be empty") + return + } + bt, err := time.Parse(base.DateTimeLayout, beginTime) + if err != nil { + base.HandleError(err) + return + } + et, err := time.Parse(base.DateTimeLayout, endTime) + if err != nil { + base.HandleError(err) + return + } + + req := base.BizClient.NewBackupUDBInstanceSlowLogRequest() + req.BeginTime = sdk.Int(int(bt.Unix())) + req.EndTime = sdk.Int(int(et.Unix())) + req.DBId = &udbID + req.BackupName = &name + req.Region = ®ion + req.ProjectId = &project + + _, err = base.BizClient.BackupUDBInstanceSlowLog(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintf(out, "mysql log archive[%s] created\n", name) + } else if logType == "error" { + req := base.BizClient.NewBackupUDBInstanceErrorLogRequest() + req.DBId = &udbID + req.BackupName = &name + req.Region = ®ion + req.Zone = &zone + req.ProjectId = &project + + _, err := base.BizClient.BackupUDBInstanceErrorLog(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintf(out, "mysql log archive[%s] created\n", name) + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringVar(&udbID, "udb-id", "", "Required. Resource ID of UDB instance which we fetch logs from") + flags.StringVar(&name, "name", "", "Required. Name of compressed file") + flags.StringVar(&logType, "log-type", "", "Required. Type of log to package. Accept values: slow_query, error") + flags.StringVar(&beginTime, "begin-time", "", "Optional. Required when log-type is slow. For example 2019-01-02/15:04:05") + flags.StringVar(&endTime, "end-time", "", "Optional. Required when log-type is slow. For example 2019-01-02/15:04:05") + bindRegionS(®ion, flags) + bindZoneS(&zone, ®ion, flags) + bindProjectIDS(&project, flags) + + cmd.MarkFlagRequired("udb-id") + cmd.MarkFlagRequired("name") + cmd.MarkFlagRequired("log-type") + + flags.SetFlagValues("log-type", "slow_query", "error") + flags.SetFlagValuesFunc("udb-id", func() []string { + return getUDBIDList(nil, "sql", project, region, base.ConfigIns.Zone) + }) + return cmd +} + +type udbArchiveRow struct { + ArchiveID int + Name string + LogType string + DB string + Size string + Status string + CreateTime string +} + +// NewCmdUDBLogArchiveList ucloud udb log archive list +func NewCmdUDBLogArchiveList(out io.Writer) *cobra.Command { + var beginTime, endTime string + logTypes := []string{} + logTypeMap := map[string]int{ + "binlog": 2, + "slow_query": 3, + "error": 4, + } + rLogTypeMap := map[int]string{ + 2: "binlog", + 3: "slow_query", + 4: "error", + } + req := base.BizClient.NewDescribeUDBLogPackageRequest() + cmd := &cobra.Command{ + Use: "list", + Short: "List mysql log archives(log files)", + Long: "List mysql log archives(log files)", + Run: func(c *cobra.Command, args []string) { + if beginTime != "" { + bt, err := time.Parse(base.DateTimeLayout, beginTime) + if err != nil { + base.HandleError(err) + return + } + req.BeginTime = sdk.Int(int(bt.Unix())) + } + if endTime != "" { + et, err := time.Parse(base.DateTimeLayout, endTime) + if err != nil { + base.HandleError(err) + return + } + req.EndTime = sdk.Int(int(et.Unix())) + } + + if *req.DBId != "" { + *req.DBId = base.PickResourceID(*req.DBId) + } + + for _, s := range logTypes { + if v, ok := logTypeMap[s]; ok { + req.Types = append(req.Types, v) + } else { + fmt.Fprintln(out, "Error, log-type should be one of 'binlog', 'slow_query' or 'error'") + } + } + + resp, err := base.BizClient.DescribeUDBLogPackage(req) + if err != nil { + base.HandleError(err) + return + } + list := []udbArchiveRow{} + for _, ins := range resp.DataSet { + row := udbArchiveRow{ + ArchiveID: ins.BackupId, + Name: ins.BackupName, + LogType: rLogTypeMap[ins.BackupType], + DB: fmt.Sprintf("%s|%s", ins.DBId, ins.DBName), + Size: fmt.Sprintf("%dB", ins.BackupSize), + Status: ins.State, + CreateTime: base.FormatDateTime(ins.BackupTime), + } + list = append(list, row) + } + base.PrintList(list, out) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&logTypes, "log-type", nil, "Optional. Type of log. Accept Values: binlog, slow_query and error") + req.DBId = flags.String("udb-id", "", "Optional. Resource ID of UDB instance which the listed logs belong to") + flags.StringVar(&beginTime, "begin-time", "", "Optional. For example 2019-01-02/15:04:05") + flags.StringVar(&endTime, "end-time", "", "Optional. For example 2019-01-02/15:04:05") + bindProjectID(req, flags) + bindRegion(req, flags) + bindZone(req, flags) + bindLimit(req, flags) + bindOffset(req, flags) + + flags.SetFlagValues("log-type", "binlog", "slow_query", "error") + flags.SetFlagValuesFunc("udb-id", func() []string { + return getUDBIDList(nil, "sql", *req.ProjectId, *req.Region, *req.Zone) + }) + + return cmd +} + +// NewCmdUDBLogArchiveGetDownloadURL ucloud udb log archive get-download-url +func NewCmdUDBLogArchiveGetDownloadURL(out io.Writer) *cobra.Command { + req := base.BizClient.NewDescribeUDBBinlogBackupURLRequest() + cmd := &cobra.Command{ + Use: "download", + Short: "Display url of an archive(log file)", + Long: "Display url of an archive(log file)", + Example: "ucloud mysql logs download --udb-id udb-urixxx/test.cli1 --archive-id 35044", + Run: func(c *cobra.Command, args []string) { + *req.DBId = base.PickResourceID(*req.DBId) + resp, err := base.BizClient.DescribeUDBBinlogBackupURL(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintln(out, resp.BackupPath) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + req.BackupId = flags.Int("archive-id", 0, "Required. ArchiveID of archive to download") + req.DBId = flags.String("udb-id", "", "Required. Resource ID of UDB which the archive belongs to") + bindRegion(req, flags) + bindZone(req, flags) + bindProjectID(req, flags) + + cmd.MarkFlagRequired("archive-id") + cmd.MarkFlagRequired("udb-id") + + flags.SetFlagValuesFunc("udb-id", func() []string { + return getUDBIDList(nil, "sql", *req.ProjectId, *req.Region, *req.Zone) + }) + + return cmd +} + +// NewCmdUDBLogArchiveDelete ucloud udb log archive delete +func NewCmdUDBLogArchiveDelete(out io.Writer) *cobra.Command { + var ids []int + req := base.BizClient.NewDeleteUDBLogPackageRequest() + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete log archives(log files)", + Long: "Delete log archives(log files)", + Example: "ucloud mysql logs delete --archive-id 35025", + Run: func(c *cobra.Command, args []string) { + for _, id := range ids { + req.BackupId = sdk.Int(id) + _, err := base.BizClient.DeleteUDBLogPackage(req) + if err != nil { + base.HandleError(err) + continue + } + fmt.Fprintf(out, "archive[%d] deleted\n", id) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + flags.IntSliceVar(&ids, "archive-id", nil, "Optional. ArchiveID of log archives to delete") + bindRegion(req, flags) + bindZone(req, flags) + bindProjectID(req, flags) + + cmd.MarkFlagRequired("archive-id") + + return cmd +} diff --git a/cmd/bandwidth.go b/cmd/bandwidth.go new file mode 100644 index 0000000000..703d2c1243 --- /dev/null +++ b/cmd/bandwidth.go @@ -0,0 +1,405 @@ +// Copyright © 2018 NAME HERE tony.li@ucloud.cn +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "io" + "strconv" + "strings" + "time" + + "github.com/spf13/cobra" + + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + + "github.com/ucloud/ucloud-cli/base" + "github.com/ucloud/ucloud-cli/model/status" +) + +// NewCmdBandwidth ucloud bw +func NewCmdBandwidth() *cobra.Command { + cmd := &cobra.Command{ + Use: "bw", + Short: "Manipulate bandwidth package and shared bandwidth", + Long: "Manipulate bandwidth package and shared bandwidth", + } + cmd.AddCommand(NewCmdBandwidthPkg()) + cmd.AddCommand(NewCmdSharedBW()) + return cmd +} + +// NewCmdSharedBW ucloud shared-bw +func NewCmdSharedBW() *cobra.Command { + cmd := &cobra.Command{ + Use: "shared", + Short: "Create and manipulate shared bandwidth instances", + Long: "Create and manipulate shared bandwidth instances", + } + out := base.Cxt.GetWriter() + cmd.AddCommand(NewCmdSharedBWCreate()) + cmd.AddCommand(NewCmdSharedBWList(out)) + cmd.AddCommand(NewCmdSharedBWResize()) + cmd.AddCommand(NewCmdSharedBWDelete()) + return cmd +} + +// NewCmdSharedBWCreate ucloud shared-bw create +func NewCmdSharedBWCreate() *cobra.Command { + req := base.BizClient.NewAllocateShareBandwidthRequest() + cmd := &cobra.Command{ + Use: "create", + Short: "Create shared bandwidth instance", + Long: "Create shared bandwidth instance", + Run: func(c *cobra.Command, args []string) { + if *req.ShareBandwidth < 20 || *req.ShareBandwidth > 5000 { + base.Cxt.Printf("bandwidth should be between 20 and 5000. received %d\n", *req.ShareBandwidth) + return + } + resp, err := base.BizClient.AllocateShareBandwidth(req) + if err != nil { + base.HandleError(err) + return + } + base.Cxt.Printf("shared bandwidth[%s] created\n", resp.ShareBandwidthId) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + req.Name = flags.String("name", "", "Required. Name of the shared bandwidth instance") + req.ShareBandwidth = flags.Int("bandwidth-mb", 20, "Optional. Unit:Mb. Bandwidth of the shared bandwidth. Range [20,5000]") + bindRegion(req, flags) + bindProjectID(req, flags) + req.ChargeType = flags.String("charge-type", "Month", "Optional.'Year',pay yearly;'Month',pay monthly;'Dynamic', pay hourly") + req.Quantity = flags.Int("quantity", 1, "Optional. The duration of the instance. N years/months.") + flags.SetFlagValues("charge-type", "Month", "Year", "Dynamic") + + cmd.MarkFlagRequired("name") + + return cmd +} + +// SharedBWRow 表格行 +type SharedBWRow struct { + Name string + ResourceID string + ChargeType string + Bandwidth string + EIP string + ExpirationTime string +} + +// NewCmdSharedBWList ucloud shared-bw list +func NewCmdSharedBWList(out io.Writer) *cobra.Command { + req := base.BizClient.NewDescribeShareBandwidthRequest() + cmd := &cobra.Command{ + Use: "list", + Short: "List shared bandwidth instances", + Long: "List shared bandwidth instances", + Run: func(c *cobra.Command, args []string) { + resp, err := base.BizClient.DescribeShareBandwidth(req) + if err != nil { + base.HandleError(err) + return + } + list := []SharedBWRow{} + for _, sb := range resp.DataSet { + row := SharedBWRow{} + row.Name = sb.Name + row.ResourceID = sb.ShareBandwidthId + row.ChargeType = sb.ChargeType + row.Bandwidth = strconv.Itoa(sb.ShareBandwidth) + "Mb" + row.ExpirationTime = base.FormatDate(sb.ExpireTime) + eipList := []string{} + for _, eip := range sb.EIPSet { + eipText := "" + eipText += eip.EIPId + for _, ip := range eip.EIPAddr { + eipText += fmt.Sprintf("/%s/%s", ip.IP, ip.OperatorName) + } + eipList = append(eipList, eipText) + } + row.EIP = strings.Join(eipList, "\n") + list = append(list, row) + } + base.PrintList(list, out) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + bindRegion(req, flags) + bindProjectID(req, flags) + flags.StringSliceVar(&req.ShareBandwidthIds, "shared-bw-id", nil, "Resource ID of shared bandwidth instances to list") + + return cmd +} + +// NewCmdSharedBWResize ucloud shared-bw resize +func NewCmdSharedBWResize() *cobra.Command { + req := base.BizClient.NewResizeShareBandwidthRequest() + cmd := &cobra.Command{ + Use: "resize", + Short: "Resize shared bandwidth instance's bandwidth", + Long: "Resize shared bandwidth instance's bandwidth", + Run: func(c *cobra.Command, args []string) { + if *req.ShareBandwidth < 20 || *req.ShareBandwidth > 5000 { + base.Cxt.Printf("bandwidth should be between 20 and 5000. received %d\n", *req.ShareBandwidth) + return + } + req.ShareBandwidthId = sdk.String(base.PickResourceID(*req.ShareBandwidthId)) + _, err := base.BizClient.ResizeShareBandwidth(req) + if err != nil { + base.HandleError(err) + return + } + base.Cxt.Printf("shared bandwidth[%s] resized to %dMb\n", *req.ShareBandwidthId, *req.ShareBandwidth) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + req.ShareBandwidthId = flags.String("shared-bw-id", "", "Required. Resource ID of shared bandwidth instance to resize") + req.ShareBandwidth = flags.Int("bandwidth-mb", 0, "Required. Unit:Mb. resize to bandwidth value") + bindRegion(req, flags) + bindProjectID(req, flags) + + flags.SetFlagValuesFunc("shared-bw-id", func() []string { + list, _ := getAllSharedBW(*req.ProjectId, *req.Region) + return list + }) + + cmd.MarkFlagRequired("shared-bw-id") + cmd.MarkFlagRequired("bandwidth-mb") + + return cmd +} + +// NewCmdSharedBWDelete ucloud shared-bw delete +func NewCmdSharedBWDelete() *cobra.Command { + req := base.BizClient.NewReleaseShareBandwidthRequest() + ids := []string{} + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete shared bandwidth instance", + Long: "Delete shared bandwidth instance", + Run: func(c *cobra.Command, args []string) { + for _, id := range ids { + req.ShareBandwidthId = sdk.String(base.PickResourceID(id)) + _, err := base.BizClient.ReleaseShareBandwidth(req) + if err != nil { + base.HandleError(err) + continue + } + base.Cxt.Printf("shared bandwidth[%s] deleted\n", id) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&ids, "shared-bw-id", nil, "Required. Resource ID of shared bandwidth instances to delete") + req.EIPBandwidth = flags.Int("eip-bandwidth-mb", 1, "Optional. Bandwidth of the joined EIPs,after deleting the shared bandwidth instance") + req.PayMode = flags.String("traffic-mode", "", "Optional. The charge mode of joined EIPs after deleting the shared bandwidth. Accept values:Bandwidth,Traffic") + bindRegion(req, flags) + bindProjectID(req, flags) + flags.SetFlagValuesFunc("shared-bw-id", func() []string { + list, _ := getAllSharedBW(*req.ProjectId, *req.Region) + return list + }) + flags.SetFlagValues("traffic-mode", "Bandwidth", "Traffic") + + cmd.MarkFlagRequired("shared-bw-id") + + return cmd +} + +func getAllSharedBW(project, region string) ([]string, error) { + req := base.BizClient.NewDescribeShareBandwidthRequest() + req.ProjectId = &project + req.Region = ®ion + resp, err := base.BizClient.DescribeShareBandwidth(req) + if err != nil { + return nil, err + } + list := []string{} + for _, item := range resp.DataSet { + list = append(list, item.ShareBandwidthId+"/"+item.Name) + } + return list, nil +} + +// NewCmdBandwidthPkg ucloud bw-pkg +func NewCmdBandwidthPkg() *cobra.Command { + cmd := &cobra.Command{ + Use: "pkg", + Short: "List, create and delete bandwidth package instances", + Long: "List, create and delete bandwidth package instances", + } + out := base.Cxt.GetWriter() + cmd.AddCommand(NewCmdBandwidthPkgCreate()) + cmd.AddCommand(NewCmdBandwidthPkgList(out)) + cmd.AddCommand(NewCmdBandwidthPkgDelete()) + return cmd +} + +// NewCmdBandwidthPkgCreate ucloud bw-pkg create +func NewCmdBandwidthPkgCreate() *cobra.Command { + var start, end *string + timeLayout := "2006-01-02/15:04:05" + ids := []string{} + req := base.BizClient.NewCreateBandwidthPackageRequest() + loc, _ := time.LoadLocation("Local") + cmd := &cobra.Command{ + Use: "create", + Short: "Create bandwidth package", + Long: "Create bandwidth package", + Example: "ucloud bw pkg create --eip-id eip-xxx --bandwidth-mb 20 --start-time 2018-12-15/09:20:00 --end-time 2018-12-16/09:20:00", + Run: func(c *cobra.Command, args []string) { + st, err := time.ParseInLocation(timeLayout, *start, loc) + if err != nil { + base.HandleError(err) + return + } + et, err := time.ParseInLocation(timeLayout, *end, loc) + if err != nil { + base.HandleError(err) + return + } + if st.Sub(time.Now()) < 0 { + base.Cxt.Println("start-time must be after the current time") + return + } + du := et.Unix() - st.Unix() + if du <= 0 { + base.Cxt.Println("end-time must be after the start-time") + return + } + req.EnableTime = sdk.Int(int(st.Unix())) + req.TimeRange = sdk.Int(int(du)) + + for _, id := range ids { + id = base.PickResourceID(id) + req.EIPId = &id + resp, err := base.BizClient.CreateBandwidthPackage(req) + if err != nil { + base.HandleError(err) + continue + } + base.Cxt.Printf("bandwidth package[%s] created for eip[%s]\n", resp.BandwidthPackageId, id) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + flags.StringSliceVar(&ids, "eip-id", nil, "Required. Resource ID of eip to be bound with created bandwidth package") + start = flags.String("start-time", "", "Required. The time to enable bandwidth package. Local time, for example '2018-12-25/08:30:00'") + end = flags.String("end-time", "", "Required. The time to disable bandwidth package. Local time, for example '2018-12-26/08:30:00'") + req.Bandwidth = flags.Int("bandwidth-mb", 0, "Required. bandwidth of the bandwidth package to create.Range [1,800]. Unit:'Mb'.") + bindRegion(req, flags) + bindProjectID(req, flags) + + cmd.Flags().SetFlagValuesFunc("eip-id", func() []string { + return getAllEip(*req.ProjectId, *req.Region, []string{status.EIP_USED}, []string{status.EIP_CHARGE_BANDWIDTH}) + }) + + cmd.MarkFlagRequired("eip-id") + cmd.MarkFlagRequired("start-time") + cmd.MarkFlagRequired("end-time") + cmd.MarkFlagRequired("bandwidth-mb") + return cmd +} + +// BandwidthPkgRow 表格行 +type BandwidthPkgRow struct { + ResourceID string + EIP string + Bandwidth string + StartTime string + EndTime string +} + +// NewCmdBandwidthPkgList ucloud bw-pkg list +func NewCmdBandwidthPkgList(out io.Writer) *cobra.Command { + req := base.BizClient.NewDescribeBandwidthPackageRequest() + cmd := &cobra.Command{ + Use: "list", + Short: "List bandwidth packages", + Long: "List bandwidth packages", + Run: func(c *cobra.Command, args []string) { + resp, err := base.BizClient.DescribeBandwidthPackage(req) + if err != nil { + base.HandleError(err) + return + } + list := []BandwidthPkgRow{} + for _, bp := range resp.DataSets { + row := BandwidthPkgRow{ + ResourceID: bp.BandwidthPackageId, + Bandwidth: strconv.Itoa(bp.Bandwidth) + "MB", + StartTime: base.FormatDateTime(bp.EnableTime), + EndTime: base.FormatDateTime(bp.DisableTime), + } + eip := bp.EIPId + for _, addr := range bp.EIPAddr { + eip += "/" + addr.IP + "/" + addr.OperatorName + } + row.EIP = eip + list = append(list, row) + } + base.PrintList(list, out) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + bindRegion(req, flags) + bindProjectID(req, flags) + req.Offset = cmd.Flags().Int("offset", 0, "Optional. Offset") + req.Limit = cmd.Flags().Int("limit", 50, "Optional. Limit range [0,10000000]") + + return cmd +} + +// NewCmdBandwidthPkgDelete ucloud bw-pkg delete +func NewCmdBandwidthPkgDelete() *cobra.Command { + ids := []string{} + req := base.BizClient.NewDeleteBandwidthPackageRequest() + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete bandwidth packages", + Long: "Delete bandwidth packages", + Example: "ucloud bw pkg delete --resource-id bwpack-xxx", + Run: func(c *cobra.Command, args []string) { + for _, id := range ids { + id := base.PickResourceID(id) + req.BandwidthPackageId = &id + _, err := base.BizClient.DeleteBandwidthPackage(req) + if err != nil { + base.HandleError(err) + return + } + base.Cxt.Printf("bandwidth package[%s] deleted\n", id) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + flags.StringSliceVar(&ids, "resource-id", nil, "Required, Resource ID of bandwidth package to delete") + bindRegion(req, flags) + bindProjectID(req, flags) + + return cmd +} diff --git a/cmd/completion.go b/cmd/completion.go index 386516f5c8..969e070c82 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -23,7 +23,8 @@ import ( "strings" "github.com/spf13/cobra" - . "github.com/ucloud/ucloud-cli/base" + + "github.com/ucloud/ucloud-cli/base" ) // NewCmdCompletion ucloud completion @@ -40,7 +41,7 @@ func NewCmdCompletion() *cobra.Command { } else if strings.HasSuffix(shell, "zsh") { zshCompletion(cmd) } else { - fmt.Println("Unknow shell: %", shell) + fmt.Printf("So far, shell %s is not supported\n", shell) } } else { fmt.Println("Lookup shell failed") @@ -53,40 +54,25 @@ func NewCmdCompletion() *cobra.Command { func bashCompletion(cmd *cobra.Command) { platform := runtime.GOOS if platform == "darwin" { - fmt.Println(`Please append 'complete -C /usr/local/bin/ucloud ucloud' to file '~/.bash_profile' -If the following scripts are included in '~/.bash_profile', please remove it. Those scripts used to auto complete words before ucloud cli v0.1.3" - -if [ -f $(brew --prefix)/etc/bash_completion ]; then - . $(brew --prefix)/etc/bash_completion -fi -source ~/.ucloud/ucloud.sh`) + fmt.Println(`Please append 'complete -C $(which ucloud) ucloud' to file '~/.bash_profile'`) } else if platform == "linux" { - fmt.Println(`Please append 'complete -C /usr/local/bin/ucloud ucloud' to file '~/.bashrc' -If the following scripts are included in '~/.bashrc', please remove it. Those scripts used to auto complete words before ucloud cli v0.1.3" - -if [ -f /etc/bash_completion ]; then - . /etc/bash_completion -fi - -source ~/.ucloud/ucloud.sh`) + fmt.Println(`Please append 'complete -C $(which ucloud) ucloud' to file '~/.bashrc'`) } } func zshCompletion(cmd *cobra.Command) { fmt.Println(`Please append the following scripts to file '~/.zshrc'. + autoload -U +X bashcompinit && bashcompinit -complete -F /usr/local/bin/ucloud ucloud`) - fmt.Println("If the following scripts are included in '~/.bash_profile' or '~/.bashrc', please remove it. The scripts used to auto complete words before ucloud cli v0.1.3") - fmt.Printf("fpath=(~/%s $fpath)\n", ConfigPath) - fmt.Println("autoload -U +X compinit && compinit") +complete -F $(which ucloud) ucloud`) } func getBashVersion() (version string, err error) { lookupBashVersion := exec.Command("bash", "-version") out, err := lookupBashVersion.Output() if err != nil { - Cxt.PrintErr(err) + base.Cxt.PrintErr(err) } // Example diff --git a/cmd/configure.go b/cmd/configure.go index 8ca62f6386..fdc5682ed0 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -15,18 +15,19 @@ package cmd import ( - "reflect" - - "github.com/ucloud/ucloud-cli/ux" + "errors" + "fmt" + "strconv" "github.com/spf13/cobra" - . "github.com/ucloud/ucloud-cli/base" -) + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + uerr "github.com/ucloud/ucloud-sdk-go/ucloud/error" -var config = ConfigInstance + "github.com/ucloud/ucloud-cli/base" +) -const configDesc = `Public-key and private-key could be acquired from https://console.ucloud.cn/uapi/apikey.` +const configDesc = `Public-key and private-key could be acquired from https://console.ucloud.cn/uaccount/api_manage` const helloUcloud = ` _ _ _ _ _ _ _____ _ _ @@ -35,51 +36,69 @@ const helloUcloud = ` | _ |/ _ \ | |/ _ \ | | | | | | |/ _ \| | | |/ _\ | | | | | __/ | | (_) | | |_| | \__/\ | (_) | |_| | (_| | \_| |_/\___|_|_|\___/ \___/ \____/_|\___/ \__,_|\__,_| - ` -//NewCmdInit ucloud init +If you want add or modify your configurations, run 'ucloud config add/update' +` + +// NewCmdInit ucloud init func NewCmdInit() *cobra.Command { cmd := &cobra.Command{ Use: "init", Short: "Initialize UCloud CLI options", Long: `Initialize UCloud CLI options such as private-key,public-key,default region,zone and project.`, Run: func(cmd *cobra.Command, args []string) { - Cxt.Println(configDesc) - if len(config.PrivateKey) != 0 && len(config.PublicKey) != 0 { - confirm, err := ux.Prompt("Your have already configured public-key and private-key. Do you want to overwrite it? (y/n):") - if err != nil { - Cxt.Println(err) - return - } - if !confirm { - printHello() - return - } + if base.ConfigIns.PrivateKey != "" && base.ConfigIns.PublicKey != "" { + printHello() + return } - config.ClearConfig() - ClientConfig.Region = "" - ClientConfig.ProjectId = "" - config.ConfigPublicKey() - config.ConfigPrivateKey() - region, zone, err := getDefaultRegion() + fmt.Println(configDesc) + base.ConfigIns.ConfigPublicKey() + base.ConfigIns.ConfigPrivateKey() + base.ConfigIns.ConfigBaseURL() + + region, err := fetchRegionWithConfig(base.ConfigIns) if err != nil { - Cxt.Println(err) + if uErr, ok := err.(uerr.Error); ok { + if uErr.Code() == 172 { + fmt.Println("public key or private key is invalid.") + return + } + } + fmt.Println(err) return } - config.Region = region - config.Zone = zone - Cxt.Printf("Configured default region:%s zone:%s\n", region, zone) + base.ConfigIns.Region = region.DefaultRegion + base.ConfigIns.Zone = region.DefaultZone + fmt.Printf("Configured default region:%s zone:%s\n", region.DefaultRegion, region.DefaultZone) - projectId, projectName, err := getDefaultProject() - if err != nil { - Cxt.Println(err) + projectID, projectName, err := getDefaultProjectWithConfig(base.ConfigIns) + if err != nil && !errors.Is(err, errNoDefaultProject) { + base.HandleError(err) return } - config.ProjectID = projectId - Cxt.Printf("Configured default project:%s %s\n", projectId, projectName) - config.SaveConfig() - printHello() + if projectID != "" && projectName != "" { + base.ConfigIns.ProjectID = projectID + fmt.Printf("Configured default project:%s %s\n", projectID, projectName) + } else { + fmt.Println("No default project, skip.") + } + base.ConfigIns.Timeout = base.DefaultTimeoutSec + base.ConfigIns.BaseURL = base.DefaultBaseURL + base.ConfigIns.MaxRetryTimes = sdk.Int(base.DefaultMaxRetryTimes) + base.ConfigIns.Active = true + fmt.Printf("Configured default base url:%s\n", base.ConfigIns.BaseURL) + fmt.Printf("Configured default timeout_sec:%ds\n", base.ConfigIns.Timeout) + fmt.Printf("Active profile name:%s\n", base.ConfigIns.Profile) + fmt.Println("You can change the default settings by running 'ucloud config update'") + base.ConfigIns.ConfigUploadLog() + err = base.AggConfigListIns.Append(base.ConfigIns) + if err != nil { + base.HandleError(fmt.Errorf("Error: %v", err)) + } else { + base.InitConfig() + printHello() + } }, } return cmd @@ -87,114 +106,485 @@ func NewCmdInit() *cobra.Command { func printHello() { userInfo, err := getUserInfo() - Cxt.Printf("You are logged in as: [%s]\n", userInfo.UserEmail) + if err != nil { + base.Cxt.PrintErr(err) + return + } + base.Cxt.Printf("You are logged in as: [%s]\n", userInfo.UserEmail) certified := isUserCertified(userInfo) + if !certified { + base.Cxt.Println("\nWarning: Please authenticate the account with your valid documentation at 'https://accountv2.ucloud.cn/authentication'.") + } + base.Cxt.Println(helloUcloud) +} + +// 根据用户设置的region和zone,检查其合法性,补上缺失的部分,给出一个合理的符合用户本意设置的region和zone +func getReasonableRegionZone(cfg *base.AggConfig) (string, string, error) { + userRegion := cfg.Region + userZone := cfg.Zone + //如果zone设置了,region不能为空,因为这种情况较难判断给出一个合理的region + if userRegion == "" && userZone != "" { + return "", "", fmt.Errorf("region is needed if zone is assigned") + } + + regionIns, err := fetchRegionWithConfig(cfg) if err != nil { - Cxt.PrintErr(err) - } else if certified == false { - Cxt.Println("\nWarning: Please authenticate the account with your valid documentation at 'https://accountv2.ucloud.cn/authentication'.") + return "", "", err + } + + if userRegion == "" && userZone == "" { + userRegion = regionIns.DefaultRegion + userZone = regionIns.DefaultZone + } + + zones, ok := regionIns.Labels[userRegion] + if !ok { + return "", "", fmt.Errorf("region[%s] is not exist! See 'ucloud region'", userRegion) + } + + if userZone != "" { + zoneExist := false + for _, zone := range zones { + if zone == userZone { + zoneExist = true + } + } + if !zoneExist { + return "", "", fmt.Errorf("zone[%s] not exist in region[%s]! See 'ucloud config list' and 'ucloud region'", userZone, userRegion) + } + } else if len(zones) > 0 { + userZone = zones[0] } - Cxt.Println(helloUcloud) + + return userRegion, userZone, nil } -//NewCmdConfig ucloud config +// NewCmdConfig ucloud config func NewCmdConfig() *cobra.Command { - cfg := Config{} + var active, upload string + cfg := base.AggConfig{} cmd := &cobra.Command{ Use: "config", - Short: "Configure UCloud CLI options", - Long: `Configure UCloud CLI options such as private-key,public-key,default region and default project-id.`, - Example: "ucloud config list; ucloud config --region cn-bj2", - Run: func(cmd *cobra.Command, args []string) { - if cfg.Region != "" || cfg.Zone != "" { - regionMap, err := fetchRegion() - if err != nil { - HandleError(err) - return + Short: "add or update configurations", + Long: `add or update configurations, such as private-key, public-key, default region and zone, base-url, timeout-sec, and default project-id`, + Example: "ucloud config --profile=test --region cn-bj2 --active true", + Run: func(c *cobra.Command, args []string) { + if cfg.Profile == "" { + c.HelpFunc()(c, args) + return + } + + if cfg.Timeout < 0 { + base.HandleError(fmt.Errorf("timeout_sec must be greater than 0, accept %d", cfg.Timeout)) + return + } + + //cacheConfig AggConfig read from $HOME/.ucloud/config.json+credential.json or empty shell + cacheConfig, ok := base.AggConfigListIns.GetAggConfigByProfile(cfg.Profile) + //如果配置文件中找不到该profile 则添加配置 + if !ok { + cacheConfig = &base.AggConfig{ + PrivateKey: cfg.PrivateKey, + PublicKey: cfg.PublicKey, + Profile: cfg.Profile, + BaseURL: cfg.BaseURL, + Timeout: cfg.Timeout, + Active: cfg.Active, + Region: cfg.Region, + Zone: cfg.Zone, + ProjectID: cfg.ProjectID, + MaxRetryTimes: cfg.MaxRetryTimes, } + } - region := cfg.Region - if region == "" { - region = config.Region + if cfg.PrivateKey != "" { + cacheConfig.PrivateKey = cfg.PrivateKey + } + if cfg.PublicKey != "" { + cacheConfig.PublicKey = cfg.PublicKey + } + + if cfg.BaseURL == "" { + if cacheConfig.BaseURL == "" { + cacheConfig.BaseURL = base.DefaultBaseURL } + } else { + cacheConfig.BaseURL = cfg.BaseURL + } - zones, ok := regionMap[region] - if !ok { - Cxt.Printf("Error, region[%s] not exist! See 'ucloud region'\n", region) - return + if cfg.Timeout == 0 { + if cacheConfig.Timeout == 0 { + cacheConfig.Timeout = base.DefaultTimeoutSec } + } else { + cacheConfig.Timeout = cfg.Timeout + } - zone := cfg.Zone - if zone == "" { - zone = config.Zone + if *cfg.MaxRetryTimes == 0 { + if *cacheConfig.MaxRetryTimes == 0 { + cacheConfig.MaxRetryTimes = sdk.Int(base.DefaultMaxRetryTimes) } + } else { + cacheConfig.MaxRetryTimes = cfg.MaxRetryTimes + } - if zone != "" { - zoneExist := false - for _, zone := range zones { - if zone == cfg.Zone { - zoneExist = true - } + if cfg.Region != "" { + cacheConfig.Region = cfg.Region + } + if cfg.Zone != "" { + cacheConfig.Zone = cfg.Zone + } + + //确保设置的Region和Zone真实存在 + region, zone, err := getReasonableRegionZone(cacheConfig) + if err != nil { + base.HandleError(fmt.Errorf("verify region failed: %v", err)) + } else { + cacheConfig.Region = region + cacheConfig.Zone = zone + } + + //如果用户填写的project和配置文件中该配置的project均为空,则调接口拉取默认project + //如果用户填写的project不为空,则校验其是否真实存在; + if cfg.ProjectID == "" { + if cacheConfig.ProjectID == "" { + id, _, err := getDefaultProjectWithConfig(cacheConfig) + if err != nil { + base.HandleError(fmt.Errorf("fetch default project failed: %v", err)) + } else { + cacheConfig.ProjectID = id } - if !zoneExist { - Cxt.Printf("Error, zone[%s] not exist in region[%s]! See 'ucloud config list' and 'ucloud region'\n", zone, region) - return + } + } else { + cfg.ProjectID = base.PickResourceID(cfg.ProjectID) + projects, err := fetchProjectWithConfig(cacheConfig) + if err != nil { + cacheConfig.ProjectID = cfg.ProjectID + } else { + if ok := projects[cfg.ProjectID]; ok { + cacheConfig.ProjectID = cfg.ProjectID + } else { + base.HandleError(fmt.Errorf("project %s you assigned not exists", cfg.ProjectID)) + if ok := projects[cacheConfig.ProjectID]; !ok { + base.HandleError(fmt.Errorf("project %s not exists, assign another one please", cacheConfig.ProjectID)) + } } } } - tmpCfgVal := reflect.ValueOf(cfg) - configVal := reflect.ValueOf(config).Elem() - changed := false - for i := 0; i < tmpCfgVal.NumField(); i++ { - if fieldVal := tmpCfgVal.Field(i).String(); fieldVal != "" { - configVal.Field(i).SetString(fieldVal) - changed = true + if active != "" { + if active == "true" { + cacheConfig.Active = true + } else if active == "false" { + cacheConfig.Active = false + } else { + base.HandleError(fmt.Errorf("flag active should be true or false. received %s", active)) } } - if changed { - config.SaveConfig() - } else { - cmd.HelpFunc()(cmd, args) + + if upload != "" { + if upload == "true" { + cacheConfig.AgreeUploadLog = true + } else if upload == "false" { + cacheConfig.AgreeUploadLog = false + } else { + base.HandleError(fmt.Errorf("flag agree-upload-log should be true or false. received %s", active)) + } + } + + err = base.AggConfigListIns.UpdateAggConfig(cacheConfig) + if err != nil { + base.HandleError(err) } }, } flags := cmd.Flags() flags.SortFlags = false + flags.StringVar(&cfg.Profile, "profile", "", "Required. Set name of CLI profile") flags.StringVar(&cfg.PublicKey, "public-key", "", "Optional. Set public key") flags.StringVar(&cfg.PrivateKey, "private-key", "", "Optional. Set private key") flags.StringVar(&cfg.Region, "region", "", "Optional. Set default region. For instance 'cn-bj2' See 'ucloud region'") flags.StringVar(&cfg.Zone, "zone", "", "Optional. Set default zone. For instance 'cn-bj2-02'. See 'ucloud region'") flags.StringVar(&cfg.ProjectID, "project-id", "", "Optional. Set default project. For instance 'org-xxxxxx'. See 'ucloud project list") + flags.StringVar(&cfg.BaseURL, "base-url", "", "Optional. Set default base url. For instance 'https://api.ucloud.cn/'") + flags.IntVar(&cfg.Timeout, "timeout-sec", 0, "Optional. Set default timeout for requesting API. Unit: seconds") + cfg.MaxRetryTimes = flags.Int("max-retry-times", 0, "Optional. Set default max-retry-times for idempotent APIs which can be called many times without side effect, for example 'ReleaseEIP'") + flags.StringVar(&active, "active", "", "Optional. Mark the profile to be effective or not. Accept valeus: true or false") + flags.StringVar(&upload, "agree-upload-log", "false", "Optional. Agree to upload log in local file ~/.ucloud/cli.log or not. Accept valeus: true or false") + + flags.SetFlagValues("active", "true", "false") + flags.SetFlagValues("agree-upload-log", "true", "false") + flags.SetFlagValuesFunc("profile", func() []string { return base.AggConfigListIns.GetProfileNameList() }) + flags.SetFlagValuesFunc("region", getRegionList) + flags.SetFlagValuesFunc("project-id", getProjectList) + flags.SetFlagValuesFunc("zone", func() []string { + return getZoneList(cfg.Region) + }) + cmd.AddCommand(NewCmdConfigAdd()) + cmd.AddCommand(NewCmdConfigUpdate()) cmd.AddCommand(NewCmdConfigList()) - cmd.AddCommand(NewCmdConfigClear()) + cmd.AddCommand(NewCmdConfigDelete()) + return cmd +} + +// NewCmdConfigAdd ucloud config add +func NewCmdConfigAdd() *cobra.Command { + var active, upload string + cfg := &base.AggConfig{} + cmd := &cobra.Command{ + Use: "add", + Short: "add configuration", + Long: "add configuration", + Run: func(c *cobra.Command, args []string) { + region, zone, err := getReasonableRegionZone(cfg) + if err != nil { + base.HandleError(err) + } + cfg.Region = region + cfg.Zone = zone + + project, err := getReasonableProject(cfg) + if err != nil { + base.HandleError(err) + } + cfg.ProjectID = project + + if cfg.Timeout <= 0 { + base.HandleError(fmt.Errorf("timeout_sec must be greater than 0, accept %d", cfg.Timeout)) + return + } + + if active == "true" { + cfg.Active = true + } else if active == "false" { + cfg.Active = false + } else { + fmt.Printf("active should be true or false, received %s\n", active) + } + + if upload == "true" { + cfg.AgreeUploadLog = true + } else if upload == "false" { + cfg.AgreeUploadLog = false + } else { + fmt.Printf("agree-upload-log should be true or false, received %s\n", active) + } + + err = base.AggConfigListIns.Append(cfg) + if err != nil { + base.HandleError(err) + return + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + flags.StringVar(&cfg.Profile, "profile", "", "Required. Set name of CLI profile") + flags.StringVar(&cfg.PublicKey, "public-key", "", "Required. Set public key") + flags.StringVar(&cfg.PrivateKey, "private-key", "", "Required. Set private key") + flags.StringVar(&cfg.Region, "region", "", "Optional. Set default region. For instance 'cn-bj2' See 'ucloud region'") + flags.StringVar(&cfg.Zone, "zone", "", "Optional. Set default zone. For instance 'cn-bj2-02'. See 'ucloud region'") + flags.StringVar(&cfg.ProjectID, "project-id", "", "Optional. Set default project. For instance 'org-xxxxxx'. See 'ucloud project list") + flags.StringVar(&cfg.BaseURL, "base-url", base.DefaultBaseURL, "Optional. Set default base url. For instance 'https://api.ucloud.cn/'") + flags.IntVar(&cfg.Timeout, "timeout-sec", base.DefaultTimeoutSec, "Optional. Set default timeout for requesting API. Unit: seconds") + cfg.MaxRetryTimes = flags.Int("max-retry-times", base.DefaultMaxRetryTimes, "Optional. Set default max-retry-times for idempotent APIs which can be called many times without side effect, for example 'ReleaseEIP'") + flags.StringVar(&active, "active", "false", "Optional. Mark the profile to be effective or not. Accept valeus: true or false") + flags.StringVar(&upload, "agree-upload-log", "false", "Optional. Agree to upload log in local file ~/.ucloud/cli.log or not. Accept valeus: true or false") + + flags.SetFlagValues("active", "true", "false") + flags.SetFlagValues("agree-upload-log", "true", "false") + flags.SetFlagValuesFunc("profile", func() []string { return base.AggConfigListIns.GetProfileNameList() }) + flags.SetFlagValuesFunc("region", getRegionList) + flags.SetFlagValuesFunc("project-id", getProjectList) + flags.SetFlagValuesFunc("zone", func() []string { + return getZoneList(cfg.Region) + }) + + cmd.MarkFlagRequired("profile") + cmd.MarkFlagRequired("public-key") + cmd.MarkFlagRequired("private-key") return cmd } -//NewCmdConfigList ucloud config list +// NewCmdConfigUpdate ucloud config update +func NewCmdConfigUpdate() *cobra.Command { + var timeout, active, maxRetries, upload string + cfg := &base.AggConfig{} + cmd := &cobra.Command{ + Use: "update", + Short: "update configurations", + Long: "update configurations", + Run: func(c *cobra.Command, args []string) { + //cacheConfig AggConfig read from $HOME/.ucloud/config.json+credential.json or empty shell + cacheConfig, ok := base.AggConfigListIns.GetAggConfigByProfile(cfg.Profile) + if !ok { + base.HandleError(fmt.Errorf("profile %s not exist", cfg.Profile)) + return + } + + if cfg.PrivateKey != "" { + cacheConfig.PrivateKey = cfg.PrivateKey + } + if cfg.PublicKey != "" { + cacheConfig.PublicKey = cfg.PublicKey + } + + //如果配置了公私钥,则先更新让其生效, 为接下来拉取Region,Zone做准备 + if cfg.PrivateKey != "" || cfg.PublicKey != "" { + base.AggConfigListIns.UpdateAggConfig(cacheConfig) + } + + //如有设置Region和Zone,确保设置的Region和Zone真实存在 + if cfg.Region != "" { + cacheConfig.Region = cfg.Region + } + if cfg.Zone != "" { + cacheConfig.Zone = cfg.Zone + } + + region, zone, err := getReasonableRegionZone(cacheConfig) + if err != nil { + base.HandleError(err) + return + } + + cacheConfig.Region = region + cacheConfig.Zone = zone + + if cfg.ProjectID != "" { + cacheConfig.ProjectID = base.PickResourceID(cfg.ProjectID) + } + + project, err := getReasonableProject(cacheConfig) + if err != nil { + base.HandleError(err) + } + cacheConfig.ProjectID = project + + if timeout != "" { + seconds, err := strconv.Atoi(timeout) + if err != nil { + base.HandleError(fmt.Errorf("parse timeout-sec failed: %v", err)) + return + } + cacheConfig.Timeout = seconds + } + + if cacheConfig.Timeout <= 0 { + base.HandleError(fmt.Errorf("timeout-sec must be greater than 0, accept %d", cfg.Timeout)) + return + } + + if maxRetries != "" { + times, err := strconv.Atoi(maxRetries) + if err != nil { + base.HandleError(fmt.Errorf("parse max-retry-times failed: %v", err)) + return + } + cacheConfig.MaxRetryTimes = × + } + + if *cacheConfig.MaxRetryTimes < 0 { + base.HandleError(fmt.Errorf("max-retry-timesc must be greater than or equal to 0, accept %d", cfg.MaxRetryTimes)) + return + } + + if cfg.BaseURL != "" { + cacheConfig.BaseURL = cfg.BaseURL + } + + if active == "true" { + cacheConfig.Active = true + } else if active == "false" { + cacheConfig.Active = false + } + + if upload == "true" { + cacheConfig.AgreeUploadLog = true + } else if upload == "false" { + cacheConfig.AgreeUploadLog = false + } + + err = base.AggConfigListIns.UpdateAggConfig(cacheConfig) + if err != nil { + base.HandleError(err) + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + flags.StringVar(&cfg.Profile, "profile", "", "Required. Set name of CLI profile") + flags.StringVar(&cfg.PublicKey, "public-key", "", "Required. Set public key") + flags.StringVar(&cfg.PrivateKey, "private-key", "", "Required. Set private key") + flags.StringVar(&cfg.Region, "region", "", "Optional. Set default region. For instance 'cn-bj2' See 'ucloud region'") + flags.StringVar(&cfg.Zone, "zone", "", "Optional. Set default zone. For instance 'cn-bj2-02'. See 'ucloud region'") + flags.StringVar(&cfg.ProjectID, "project-id", "", "Optional. Set default project. For instance 'org-xxxxxx'. See 'ucloud project list") + flags.StringVar(&cfg.BaseURL, "base-url", "", "Optional. Set default base url. For instance 'https://api.ucloud.cn/'") + flags.StringVar(&timeout, "timeout-sec", "", "Optional. Set default timeout for requesting API. Unit: seconds") + flags.StringVar(&maxRetries, "max-retry-times", "", "Optional. Set default max retry times for idempotent APIs which can be called many times without side effect, for example 'ReleaseEIP'") + flags.StringVar(&active, "active", "", "Optional. Mark the profile to be effective") + flags.StringVar(&upload, "agree-upload-log", "", "Optional. Agree to upload log in local file ~/.ucloud/cli.log or not. Accept valeus: true or false") + + flags.SetFlagValuesFunc("profile", func() []string { return base.AggConfigListIns.GetProfileNameList() }) + flags.SetFlagValuesFunc("region", getRegionList) + flags.SetFlagValuesFunc("project-id", getProjectList) + flags.SetFlagValuesFunc("zone", func() []string { + return getZoneList(cfg.Region) + }) + flags.SetFlagValues("active", "true", "false") + flags.SetFlagValues("agree-upload-log", "true", "false") + + cmd.MarkFlagRequired("profile") + + return cmd +} + +// NewCmdConfigList ucloud config list func NewCmdConfigList() *cobra.Command { - configListCmd := &cobra.Command{ + cmd := &cobra.Command{ Use: "list", - Short: "list all settings", - Long: `list all settings`, - Run: func(cmd *cobra.Command, args []string) { - config.ListConfig(global.json) + Short: "list all configurations", + Long: `list all configurations`, + Run: func(c *cobra.Command, args []string) { + base.ListAggConfig(global.JSON) }, } - return configListCmd + return cmd } -//NewCmdConfigClear ucloud config clear -func NewCmdConfigClear() *cobra.Command { - configClearCmd := &cobra.Command{ - Use: "clear", - Short: "clear all settings", - Long: "clear all settings", - Run: func(cmd *cobra.Command, args []string) { - config.ClearConfig() +// NewCmdConfigDelete ucloud config Delete +func NewCmdConfigDelete() *cobra.Command { + var profileList []string + cmd := &cobra.Command{ + Use: "delete", + Short: "delete configurations by profile name", + Long: "delete configurations by profile name", + Example: "ucloud config delete --profile test", + Run: func(c *cobra.Command, args []string) { + profiles := base.AggConfigListIns.GetProfileNameList() + allProfileMap := make(map[string]bool) + for _, p := range profiles { + allProfileMap[p] = true + } + + for _, p := range profileList { + if allProfileMap[p] { + err := base.AggConfigListIns.DeleteByProfile(p) + if err != nil { + base.HandleError(err) + } + } else { + base.HandleError(fmt.Errorf("profile %s does not exist", p)) + } + } }, } - return configClearCmd + cmd.Flags().StringSliceVar(&profileList, "profile", nil, "Required. Name of settings item") + cmd.MarkFlagRequired("profile") + cmd.Flags().SetFlagValuesFunc("profile", func() []string { return base.AggConfigListIns.GetProfileNameList() }) + return cmd } diff --git a/cmd/disk.go b/cmd/disk.go index 96de339b2e..4f901616f3 100644 --- a/cmd/disk.go +++ b/cmd/disk.go @@ -16,10 +16,13 @@ package cmd import ( "fmt" + "io" "strings" "github.com/spf13/cobra" + "github.com/ucloud/ucloud-sdk-go/ucloud/request" + "github.com/ucloud/ucloud-sdk-go/private/services/uhost" "github.com/ucloud/ucloud-sdk-go/services/udisk" sdk "github.com/ucloud/ucloud-sdk-go/ucloud" @@ -28,29 +31,35 @@ import ( "github.com/ucloud/ucloud-cli/ux" ) -//NewCmdDisk ucloud disk +// NewCmdDisk ucloud disk func NewCmdDisk() *cobra.Command { cmd := &cobra.Command{ Use: "udisk", Short: "Read and manipulate udisk instances", Long: "Read and manipulate udisk instances", } - cmd.AddCommand(NewCmdDiskCreate()) - cmd.AddCommand(NewCmdDiskList()) - cmd.AddCommand(NewCmdDiskAttach()) - cmd.AddCommand(NewCmdDiskDetach()) + writer := base.Cxt.GetWriter() + cmd.AddCommand(NewCmdDiskCreate(writer)) + cmd.AddCommand(NewCmdDiskList(writer)) + cmd.AddCommand(NewCmdDiskAttach(writer)) + cmd.AddCommand(NewCmdDiskDetach(writer)) cmd.AddCommand(NewCmdDiskDelete()) - cmd.AddCommand(NewCmdDiskClone()) + cmd.AddCommand(NewCmdDiskClone(writer)) cmd.AddCommand(NewCmdDiskExpand()) + cmd.AddCommand(NewCmdDiskSnapshot(writer)) + cmd.AddCommand(NewCmdDiskRestore(writer)) + cmd.AddCommand(NewCmdSnapshotList(writer)) + cmd.AddCommand(NewCmdSnapshotDelete(writer)) return cmd } -//NewCmdDiskCreate ucloud udisk create -func NewCmdDiskCreate() *cobra.Command { +// NewCmdDiskCreate ucloud udisk create +func NewCmdDiskCreate(out io.Writer) *cobra.Command { var async *bool var count *int + var enableDataArk *string + var snapshotID *string req := base.BizClient.NewCreateUDiskRequest() - enableDataArk := sdk.String("false") cmd := &cobra.Command{ Use: "create", Short: "Create udisk instance", @@ -71,23 +80,57 @@ func NewCmdDiskCreate() *cobra.Command { } else if *req.DiskType == "SSD" { *req.DiskType = "SSDDataDisk" } - for i := 0; i < *count; i++ { - resp, err := base.BizClient.CreateUDisk(req) - if err != nil { - base.HandleError(err) - return + if *snapshotID != "" { + cloneReq := base.BizClient.NewCloneUDiskSnapshotRequest() + cloneReq.UDataArkMode = req.UDataArkMode + cloneReq.SourceId = snapshotID + cloneReq.ProjectId = req.ProjectId + cloneReq.Region = req.Region + cloneReq.Zone = req.Zone + cloneReq.Name = req.Name + cloneReq.Size = req.Size + cloneReq.ChargeType = req.ChargeType + cloneReq.Quantity = req.Quantity + for i := 0; i < *count; i++ { + resp, err := base.BizClient.CloneUDiskSnapshot(cloneReq) + if err != nil { + base.HandleError(err) + return + } + if count := len(resp.UDiskId); count == 1 { + text := fmt.Sprintf("udisk:%v is initializing", resp.UDiskId) + if *async { + fmt.Fprintln(out, text) + } else { + poller := base.NewSpoller(describeUdiskByID, out) + poller.Spoll(resp.UDiskId[0], text, []string{status.DISK_AVAILABLE, status.DISK_FAILED}) + } + } else if count > 1 { + base.Cxt.Printf("udisk:%v created\n", resp.UDiskId) + } else { + base.Cxt.PrintErr(fmt.Errorf("none udisk created")) + } } - if count := len(resp.UDiskId); count == 1 { - text := fmt.Sprintf("udisk:%v is initializing", resp.UDiskId) - if *async { - base.Cxt.Println(text) + } else { + for i := 0; i < *count; i++ { + resp, err := base.BizClient.CreateUDisk(req) + if err != nil { + base.HandleError(err) + return + } + if count := len(resp.UDiskId); count == 1 { + text := fmt.Sprintf("udisk:%v is initializing", resp.UDiskId) + if *async { + fmt.Fprintln(out, text) + } else { + poller := base.NewSpoller(describeUdiskByID, out) + poller.Spoll(resp.UDiskId[0], text, []string{status.DISK_AVAILABLE, status.DISK_FAILED}) + } + } else if count > 1 { + base.Cxt.Printf("udisk:%v created\n", resp.UDiskId) } else { - pollDisk(resp.UDiskId[0], *req.ProjectId, *req.Region, *req.Zone, text, []string{status.DISK_AVAILABLE, status.DISK_FAILED}) + base.Cxt.PrintErr(fmt.Errorf("none udisk created")) } - } else if count > 1 { - base.Cxt.Printf("udisk:%v created\n", resp.UDiskId) - } else { - base.Cxt.PrintErr(fmt.Errorf("none udisk created")) } } }, @@ -96,15 +139,15 @@ func NewCmdDiskCreate() *cobra.Command { flags.SortFlags = false req.Name = flags.String("name", "", "Required. Name of the udisk to create") req.Size = flags.Int("size-gb", 10, "Required. Size of the udisk to create. Unit:GB. Normal udisk [1,8000]; SSD udisk [1,4000] ") - req.ProjectId = flags.String("project-id", base.ConfigInstance.ProjectID, "Optional. Assign project-id") - req.Region = flags.String("region", base.ConfigInstance.Region, "Optional. Assign region") - req.Zone = flags.String("zone", base.ConfigInstance.Zone, "Optional. Assign availability zone") + snapshotID = flags.String("snapshot-id", "", "Optional. Resource ID of a snapshot, which will apply to the udisk being created. If you set this option, 'udisk-type' will be omitted.") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Assign region") + req.Zone = flags.String("zone", base.ConfigIns.Zone, "Optional. Assign availability zone") req.ChargeType = flags.String("charge-type", "Dynamic", "Optional.'Year',pay yearly;'Month',pay monthly;'Dynamic', pay hourly") req.Quantity = flags.Int("quantity", 1, "Optional. The duration of the instance. N years/months.") enableDataArk = flags.String("enable-data-ark", "false", "Optional. DataArk supports real-time backup, which can restore the udisk back to any moment within the last 12 hours.") req.Tag = flags.String("group", "Default", "Optional. Business group") req.DiskType = flags.String("udisk-type", "Oridinary", "Optional. 'Ordinary' or 'SSD'") - req.CouponId = flags.String("coupon-id", "", "Optional. Coupon ID, The Coupon can deduct part of the payment.See https://accountv2.ucloud.cn") async = flags.Bool("async", false, "Optional. Do not wait for the long-running operation to finish.") count = flags.Int("count", 1, "Optional. The count of udisk to create. Range [1,10]") @@ -118,7 +161,7 @@ func NewCmdDiskCreate() *cobra.Command { return cmd } -//DiskRow TableRow +// DiskRow TableRow type DiskRow struct { ResourceID string Name string @@ -133,8 +176,8 @@ type DiskRow struct { ExpirationTime string } -//NewCmdDiskList ucloud disk list -func NewCmdDiskList() *cobra.Command { +// NewCmdDiskList ucloud disk list +func NewCmdDiskList(out io.Writer) *cobra.Command { req := base.BizClient.NewDescribeUDiskRequest() typeMap := map[string]string{ "DataDisk": "Oridinary-Data-Disk", @@ -180,19 +223,15 @@ func NewCmdDiskList() *cobra.Command { } list = append(list, row) } - if global.json { - base.PrintJSON(list) - } else { - base.PrintTableS(list) - } + base.PrintList(list, out) }, } flags := cmd.Flags() flags.SortFlags = false - req.ProjectId = flags.String("project-id", base.ConfigInstance.ProjectID, "Optional. Assign project-id") - req.Region = flags.String("region", base.ConfigInstance.Region, "Optional. Assign region") - req.Zone = flags.String("zone", base.ConfigInstance.Zone, "Optional. Assign availability zone") - req.UDiskId = flags.String("resource-id", "", "Optional. Resource ID of the udisk to search") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Assign region") + req.Zone = flags.String("zone", base.ConfigIns.Zone, "Optional. Assign availability zone") + req.UDiskId = flags.String("udisk-id", "", "Optional. Resource ID of the udisk to search") req.DiskType = flags.String("udisk-type", "", "Optional. Optional. Type of the udisk to search. 'Oridinary-Data-Disk','Oridinary-System-Disk' or 'SSD-Data-Disk'") req.Offset = cmd.Flags().Int("offset", 0, "Optional. Offset") req.Limit = cmd.Flags().Int("limit", 50, "Optional. Limit") @@ -200,8 +239,8 @@ func NewCmdDiskList() *cobra.Command { return cmd } -//NewCmdDiskAttach ucloud disk attach -func NewCmdDiskAttach() *cobra.Command { +// NewCmdDiskAttach ucloud disk attach +func NewCmdDiskAttach(out io.Writer) *cobra.Command { var async *bool var udiskIDs *[]string @@ -223,9 +262,10 @@ func NewCmdDiskAttach() *cobra.Command { } text := fmt.Sprintf("udisk[%s] is attaching to uhost uhost[%s]", *req.UDiskId, *req.UHostId) if *async { - base.Cxt.Println(text) + fmt.Fprintln(out, text) } else { - pollDisk(resp.UDiskId, *req.ProjectId, *req.Region, *req.Zone, text, []string{status.DISK_INUSE, status.DISK_FAILED}) + poller := base.NewSpoller(describeUdiskByID, out) + poller.Spoll(resp.UDiskId, text, []string{status.DISK_INUSE, status.DISK_FAILED}) } } }, @@ -234,9 +274,9 @@ func NewCmdDiskAttach() *cobra.Command { flags.SortFlags = false req.UHostId = flags.String("uhost-id", "", "Required. Resource ID of the uhost instance which you want to attach the disk") udiskIDs = flags.StringSlice("udisk-id", nil, "Required. Resource ID of the udisk instances to attach") - req.ProjectId = flags.String("project-id", base.ConfigInstance.ProjectID, "Optional. Assign project-id") - req.Region = flags.String("region", base.ConfigInstance.Region, "Optional. Assign region") - req.Zone = flags.String("zone", base.ConfigInstance.Zone, "Optional. Assign availability zone") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Assign region") + req.Zone = flags.String("zone", base.ConfigIns.Zone, "Optional. Assign availability zone") async = flags.Bool("async", false, "Optional. Do not wait for the long-running operation to finish.") flags.SetFlagValuesFunc("udisk-id", func() []string { @@ -252,8 +292,8 @@ func NewCmdDiskAttach() *cobra.Command { return cmd } -//NewCmdDiskDetach ucloud udisk detach -func NewCmdDiskDetach() *cobra.Command { +// NewCmdDiskDetach ucloud udisk detach +func NewCmdDiskDetach(out io.Writer) *cobra.Command { var async, yes *bool var udiskIDs *[]string req := base.BizClient.NewDetachUDiskRequest() @@ -275,44 +315,21 @@ func NewCmdDiskDetach() *cobra.Command { } for _, id := range *udiskIDs { id = base.PickResourceID(id) - any, err := describeUdiskByID(id, *req.ProjectId, *req.Region, *req.Zone) - if err != nil { - base.HandleError(err) - continue - } - if any == nil { - base.Cxt.PrintErr(fmt.Errorf("udisk[%v] is not exist", any)) - continue - } - ins, ok := any.(*udisk.UDiskDataSet) - if !ok { - base.Cxt.PrintErr(fmt.Errorf("%#v convert to udisk failed", any)) - continue - } - req.UHostId = &ins.UHostId - req.UDiskId = &id - *req.UHostId = base.PickResourceID(*req.UHostId) - resp, err := base.BizClient.DetachUDisk(req) + err := detachUdisk(*async, id, out) if err != nil { - base.HandleError(err) + base.Cxt.Println(err) continue } - text := fmt.Sprintf("udisk[%s] is detaching from uhost[%s]", resp.UDiskId, resp.UHostId) - if *async { - base.Cxt.Println(text) - } else { - pollDisk(resp.UDiskId, *req.ProjectId, *req.Region, *req.Zone, text, []string{status.DISK_AVAILABLE, status.DISK_FAILED}) - } } }, } flags := cmd.Flags() flags.SortFlags = false udiskIDs = flags.StringSlice("udisk-id", nil, "Required. Resource ID of the udisk instances to detach") - req.ProjectId = flags.String("project-id", base.ConfigInstance.ProjectID, "Optional. Assign project-id") - req.Region = flags.String("region", base.ConfigInstance.Region, "Optional. Assign region") - req.Zone = flags.String("zone", base.ConfigInstance.Zone, "Optional. Assign availability zone") - async = flags.Bool("async", false, "Optional. Do not wait for the long-running operation to finish.") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Assign region") + req.Zone = flags.String("zone", base.ConfigIns.Zone, "Optional. Assign availability zone") + async = flags.BoolP("async", "a", false, "Optional. Do not wait for the long-running operation to finish.") yes = flags.BoolP("yes", "y", false, "Optional. Do not prompt for confirmation.") flags.SetFlagValuesFunc("udisk-id", func() []string { @@ -323,7 +340,36 @@ func NewCmdDiskDetach() *cobra.Command { return cmd } -//NewCmdDiskDelete ucloud udisk delete +func detachUdisk(async bool, udiskID string, out io.Writer) error { + any, err := describeUdiskByID(udiskID, nil) + if err != nil { + return err + } + if any == nil { + return fmt.Errorf("udisk[%v] is not exist", any) + } + ins, ok := any.(*udisk.UDiskDataSet) + if !ok { + return fmt.Errorf("%#v convert to udisk failed", any) + } + req := base.BizClient.NewDetachUDiskRequest() + req.UHostId = sdk.String(ins.UHostId) + req.UDiskId = sdk.String(udiskID) + resp, err := base.BizClient.DetachUDisk(req) + if err != nil { + return err + } + text := fmt.Sprintf("udisk[%s] is detaching from uhost[%s]", resp.UDiskId, resp.UHostId) + if async { + fmt.Fprintln(out, text) + } else { + poller := base.NewSpoller(describeUdiskByID, out) + poller.Spoll(udiskID, text, []string{status.DISK_AVAILABLE, status.DISK_FAILED}) + } + return nil +} + +// NewCmdDiskDelete ucloud udisk delete func NewCmdDiskDelete() *cobra.Command { var yes *bool var udiskIDs *[]string @@ -359,9 +405,9 @@ func NewCmdDiskDelete() *cobra.Command { flags := cmd.Flags() flags.SortFlags = false udiskIDs = flags.StringSlice("udisk-id", nil, "Required. The Resource ID of udisks to delete") - req.ProjectId = flags.String("project-id", base.ConfigInstance.ProjectID, "Optional. Assign project-id") - req.Region = flags.String("region", base.ConfigInstance.Region, "Optional. Assign region") - req.Zone = flags.String("zone", base.ConfigInstance.Zone, "Optional. Assign availability zone") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Assign region") + req.Zone = flags.String("zone", base.ConfigIns.Zone, "Optional. Assign availability zone") yes = flags.BoolP("yes", "y", false, "Optional. Do not prompt for confirmation.") flags.SetFlagValuesFunc("udisk-id", func() []string { @@ -373,8 +419,8 @@ func NewCmdDiskDelete() *cobra.Command { return cmd } -//NewCmdDiskClone ucloud disk clone -func NewCmdDiskClone() *cobra.Command { +// NewCmdDiskClone ucloud disk clone +func NewCmdDiskClone(out io.Writer) *cobra.Command { var async *bool req := base.BizClient.NewCloneUDiskRequest() enableDataArk := sdk.String("false") @@ -399,9 +445,10 @@ func NewCmdDiskClone() *cobra.Command { if len(resp.UDiskId) == 1 { text := fmt.Sprintf("cloned udisk:[%s] is initializing", resp.UDiskId[0]) if *async { - base.Cxt.Println(text) + fmt.Fprintln(out, text) } else { - pollDisk(resp.UDiskId[0], *req.ProjectId, *req.Region, *req.Zone, text, []string{status.DISK_AVAILABLE, status.DISK_FAILED}) + poller := base.NewSpoller(describeUdiskByID, out) + poller.Spoll(resp.UDiskId[0], text, []string{status.DISK_AVAILABLE, status.DISK_FAILED}) } } else { base.Cxt.Printf("udisk[%v] cloned", resp.UDiskId) @@ -412,9 +459,9 @@ func NewCmdDiskClone() *cobra.Command { flags.SortFlags = false req.SourceId = flags.String("source-id", "", "Required. Resource ID of parent udisk") req.Name = flags.String("name", "", "Required. Name of new udisk") - req.ProjectId = flags.String("project-id", base.ConfigInstance.ProjectID, "Optional. Assign project-id") - req.Region = flags.String("region", base.ConfigInstance.Region, "Optional. Assign region") - req.Zone = flags.String("zone", base.ConfigInstance.Zone, "Optional. Assign availability zone") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Assign region") + req.Zone = flags.String("zone", base.ConfigIns.Zone, "Optional. Assign availability zone") req.ChargeType = flags.String("charge-type", "Month", "Optional.'Year',pay yearly;'Month',pay monthly;'Dynamic', pay hourly") req.Quantity = flags.Int("quantity", 1, "Optional. The duration of the instance. N years/months.") enableDataArk = flags.String("enable-data-ark", "false", "Optional. DataArk supports real-time backup, which can restore the udisk back to any moment within the last 12 hours.") @@ -434,7 +481,7 @@ func NewCmdDiskClone() *cobra.Command { return cmd } -//NewCmdDiskExpand ucloud udisk expand +// NewCmdDiskExpand ucloud udisk expand func NewCmdDiskExpand() *cobra.Command { var udiskIDs *[]string req := base.BizClient.NewResizeUDiskRequest() @@ -443,10 +490,6 @@ func NewCmdDiskExpand() *cobra.Command { Short: "Expand udisk size", Long: "Expand udisk size", Run: func(cmd *cobra.Command, args []string) { - if *req.Size > 8000 || *req.Size < 1 { - base.Cxt.Println("size-gb should be between 1 and 8000") - return - } for _, id := range *udiskIDs { id = base.PickResourceID(id) req.UDiskId = &id @@ -463,10 +506,9 @@ func NewCmdDiskExpand() *cobra.Command { flags.SortFlags = false udiskIDs = flags.StringSlice("udisk-id", nil, "Required. Resource ID of the udisks to expand") req.Size = flags.Int("size-gb", 0, "Required. Size of the udisk after expanded. Unit: GB. Range [1,8000]") - req.ProjectId = flags.String("project-id", base.ConfigInstance.ProjectID, "Optional. Assign project-id") - req.Region = flags.String("region", base.ConfigInstance.Region, "Optional. Assign region") - req.Zone = flags.String("zone", base.ConfigInstance.Zone, "Optional. Assign availability zone") - req.CouponId = flags.String("coupon-id", "", "Optional. Coupon ID, The Coupon can deduct part of the payment,see https://accountv2.ucloud.cn") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Assign region") + req.Zone = flags.String("zone", base.ConfigIns.Zone, "Optional. Assign availability zone") flags.SetFlagValuesFunc("udisk-id", func() []string { return getDiskList([]string{status.DISK_AVAILABLE}, *req.ProjectId, *req.Region, *req.Zone) @@ -478,6 +520,204 @@ func NewCmdDiskExpand() *cobra.Command { return cmd } +// NewCmdDiskSnapshot ucloud udisk snapshot +func NewCmdDiskSnapshot(out io.Writer) *cobra.Command { + var async *bool + var udiskIDs *[]string + req := base.BizClient.NewCreateUDiskSnapshotRequest() + cmd := &cobra.Command{ + Use: "snapshot", + Short: "Create shapshots for udisks", + Long: "Create shapshots for udisks", + Run: func(c *cobra.Command, args []string) { + for _, id := range *udiskIDs { + id = base.PickResourceID(id) + req.UDiskId = &id + resp, err := base.BizClient.CreateUDiskSnapshot(req) + if err != nil { + base.HandleError(err) + return + } + if len(resp.SnapshotId) == 1 { + text := fmt.Sprintf("snapshot[%s] is creating", resp.SnapshotId[0]) + if *async { + fmt.Fprintln(out, text) + } else { + poller := base.NewSpoller(describeSnapshotByID, out) + poller.Spoll(resp.SnapshotId[0], text, []string{status.SNAPSHOT_NORMAL}) + } + } else { + fmt.Fprintf(out, "snapshot%v is creating. expect snapshot count 1, accept %d\n", resp.SnapshotId, len(resp.SnapshotId)) + } + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + udiskIDs = flags.StringSlice("udisk-id", nil, "Required. Resource ID of udisks to snapshot") + req.Name = flags.String("name", "", "Required. Name of snapshots") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Assign region") + req.Zone = flags.String("zone", base.ConfigIns.Zone, "Optional. Assign availability zone") + req.Comment = flags.String("comment", "", "Optional. Description of snapshots") + async = flags.BoolP("async", "a", false, "Optional. Do not wait for the long-running operation to finish.") + flags.SetFlagValuesFunc("udisk-id", func() []string { + return getDiskList([]string{status.DISK_AVAILABLE, status.DISK_INUSE}, *req.ProjectId, *req.Region, *req.Zone) + }) + cmd.MarkFlagRequired("udisk-id") + cmd.MarkFlagRequired("name") + return cmd +} + +// NewCmdDiskRestore ucloud udisk restore +func NewCmdDiskRestore(out io.Writer) *cobra.Command { + var snapshotIDs *[]string + req := base.BizClient.NewRestoreUHostDiskRequest() + cmd := &cobra.Command{ + Use: "restore", + Short: "Restore udisk from snapshot", + Long: "Restore udisk from snapshot", + Run: func(cmd *cobra.Command, args []string) { + for _, snapshotID := range *snapshotIDs { + snapshotID = base.PickResourceID(snapshotID) + any, err := describeSnapshotByID(snapshotID, nil) + if err != nil { + base.HandleError(err) + continue + } + snapshot, ok := any.(*uhost.SnapshotSet) + if !ok { + fmt.Fprintf(out, "snapshot[%s] doesn't exist\n", snapshotID) + continue + } + if snapshot.UHostId != "" { + text := fmt.Sprintf("can we detach udisk[%s] from uhost[%s]?", snapshot.DiskId, snapshot.UHostId) + sure, err := ux.Prompt(text) + if err != nil { + base.HandleError(err) + continue + } + if !sure { + continue + } + detachUdisk(false, snapshot.DiskId, out) + } + req.SnapshotIds = append(req.SnapshotIds, snapshotID) + _, err = base.BizClient.RestoreUHostDisk(req) + + if err != nil { + base.HandleError(err) + return + } + + text := fmt.Sprintf("udisk[%s] has been restored from snapshot[%s]", snapshot.DiskId, snapshot.SnapshotId) + fmt.Fprintln(out, text) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + snapshotIDs = flags.StringSlice("snapshot-id", nil, "Required. Resourece ID of the snapshots to restore from") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Assign region") + req.Zone = flags.String("zone", base.ConfigIns.Zone, "Optional. Assign availability zone") + flags.SetFlagValuesFunc("snapshot-id", func() []string { + return getSnapshotList([]string{status.SNAPSHOT_NORMAL}, *req.ProjectId, *req.Region, *req.Zone) + }) + cmd.MarkFlagRequired("snapshot-id") + return cmd +} + +// SnapshotRow 表格行 +type SnapshotRow struct { + Name string + ResourceID string + AvailabilityZone string + BoundUDisk string + Size string + State string + UDiskType string + CreationTime string +} + +// NewCmdSnapshotList ucloud udisk list-snapshot +func NewCmdSnapshotList(out io.Writer) *cobra.Command { + req := base.BizClient.NewDescribeSnapshotRequest() + cmd := &cobra.Command{ + Use: "list-snapshot", + Short: "List snaphosts", + Long: "List snaphosts", + Run: func(c *cobra.Command, args []string) { + resp, err := base.BizClient.DescribeSnapshot(req) + if err != nil { + base.HandleError(err) + return + } + list := []SnapshotRow{} + for _, snapshot := range resp.UHostSnapshotSet { + row := SnapshotRow{ + Name: snapshot.SnapshotName, + ResourceID: snapshot.SnapshotId, + AvailabilityZone: snapshot.Zone, + BoundUDisk: snapshot.DiskId, + Size: fmt.Sprintf("%dGB", snapshot.Size), + State: snapshot.State, + UDiskType: snapshot.DiskType, + CreationTime: base.FormatDate(snapshot.CreateTime), + } + list = append(list, row) + } + base.PrintList(list, out) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Assign region") + req.Zone = flags.String("zone", base.ConfigIns.Zone, "Optional. Assign availability zone") + req.SnapshotIds = *flags.StringSlice("snaphost-id", nil, "Optional. Resource ID of snapshots to list") + req.UHostId = flags.String("uhost-id", "", "Optional. Snapshots of the uhost") + req.DiskId = flags.String("disk-id", "", "Optional. Snapshots of the udisk") + req.Offset = cmd.Flags().Int("offset", 0, "Optional. Offset") + req.Limit = cmd.Flags().Int("limit", 50, "Optional. Limit, length of snaphost list") + + return cmd +} + +// NewCmdSnapshotDelete ucloud udisk delete-snapshot +func NewCmdSnapshotDelete(out io.Writer) *cobra.Command { + var snapshotIds *[]string + req := base.BizClient.NewDeleteSnapshotRequest() + cmd := &cobra.Command{ + Use: "delete-snapshot", + Short: "Delete snapshots", + Long: "Delete snapshots", + Run: func(c *cobra.Command, args []string) { + for _, snapshotID := range *snapshotIds { + req.SnapshotId = sdk.String(base.PickResourceID(snapshotID)) + resp, err := base.BizClient.DeleteSnapshot(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintf(out, "snapshot[%s] deleted\n", resp.SnapshotId) + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Assign region") + req.Zone = flags.String("zone", base.ConfigIns.Zone, "Optional. Assign availability zone") + snapshotIds = flags.StringSlice("snaphost-id", nil, "Optional. Resource ID of snapshots to delete") + cmd.MarkFlagRequired("snapshot-id") + return cmd +} + func getDiskList(states []string, project, region, zone string) []string { req := base.BizClient.NewDescribeUDiskRequest() req.ProjectId = sdk.String(project) @@ -500,20 +740,12 @@ func getDiskList(states []string, project, region, zone string) []string { return list } -func pollDisk(resourceID, projectID, region, zone, pollText string, targetState []string) { - pollFunc := base.Poll(describeUdiskByID) - done := pollFunc(resourceID, projectID, region, zone, targetState) - ux.DotSpinner.Start(pollText) - <-done - ux.DotSpinner.Stop() -} - -func describeUdiskByID(udiskID, project, region, zone string) (interface{}, error) { +func describeUdiskByID(udiskID string, commonBase *request.CommonBase) (interface{}, error) { req := base.BizClient.NewDescribeUDiskRequest() + if commonBase != nil { + req.CommonBase = *commonBase + } req.UDiskId = sdk.String(udiskID) - req.ProjectId = sdk.String(project) - req.Region = sdk.String(region) - req.Zone = sdk.String(zone) req.Limit = sdk.Int(50) resp, err := base.BizClient.DescribeUDisk(req) if err != nil { @@ -524,3 +756,41 @@ func describeUdiskByID(udiskID, project, region, zone string) (interface{}, erro } return &resp.DataSet[0], nil } + +func getSnapshotList(states []string, project, region, zone string) []string { + req := base.BizClient.NewDescribeUDiskSnapshotRequest() + req.Limit = sdk.Int(50) + req.ProjectId = &project + req.Region = ®ion + req.Zone = &zone + resp, err := base.BizClient.DescribeUDiskSnapshot(req) + if err != nil { + return nil + } + list := []string{} + for _, snapshot := range resp.DataSet { + for _, s := range states { + if snapshot.Status == s { + list = append(list, snapshot.SnapshotId+"/"+strings.Replace(snapshot.Name, " ", "-", -1)) + } + } + } + return list +} + +func describeSnapshotByID(snapshotID string, commonBase *request.CommonBase) (interface{}, error) { + req := base.BizClient.NewDescribeSnapshotRequest() + if commonBase != nil { + req.CommonBase = *commonBase + } + req.SnapshotIds = append(req.SnapshotIds, snapshotID) + req.Limit = sdk.Int(50) + resp, err := base.BizClient.DescribeSnapshot(req) + if err != nil { + return nil, err + } + if len(resp.UHostSnapshotSet) != 1 { + return nil, nil + } + return &resp.UHostSnapshotSet[0], nil +} diff --git a/cmd/doc_md.go b/cmd/doc_md.go new file mode 100644 index 0000000000..9c7d373c0a --- /dev/null +++ b/cmd/doc_md.go @@ -0,0 +1,93 @@ +// Copyright © 2018 NAME HERE tony.li@ucloud.cn +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "io" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" + + "github.com/ucloud/ucloud-sdk-go/ucloud/log" + + "github.com/ucloud/ucloud-cli/base" +) + +// NewCmdDoc ucloud doc +func NewCmdDoc(out io.Writer) *cobra.Command { + var dir, format string + cmd := &cobra.Command{ + Use: "gendoc", + Short: "Generate documents for all commands", + Long: "Generate documents for all commands. Support markdown, rst and douku", + Run: func(c *cobra.Command, args []string) { + base.ConfigIns.Region = "" + base.ConfigIns.ProjectID = "" + base.ConfigIns.Zone = "" + rootCmd := NewCmdRoot() + addChildren(rootCmd) + switch format { + case "rst": + emptyStr := func(s string) string { return "" } + linkHandler := func(name, ref string) string { + return fmt.Sprintf(":ref:`%s <%s>`", name, ref) + } + err := doc.GenReSTTreeCustom(rootCmd, dir, emptyStr, linkHandler) + if err != nil { + log.Fatal(err) + } + + case "markdown": + err := doc.GenMarkdownTree(rootCmd, dir) + if err != nil { + log.Fatal(err) + } + case "douku": + prefix := "cli/cmd/" + err := doc.GenDoukuTree(rootCmd, dir, prefix) + printCmdIndex(rootCmd, 0, "/cli/cmd") + if err != nil { + log.Fatal(err) + } + default: + fmt.Fprintf(out, "format %s is not supported\n", format) + } + }, + } + + cmd.Flags().StringVar(&dir, "dir", "", "Required. The directory where documents of commands are stored") + cmd.Flags().StringVar(&format, "format", "douku", "Required. Format of the doucments. Accept values: markdown, rst and douku") + + cmd.Flags().SetFlagValues("format", "douku", "markdown", "rst") + cmd.Flags().SetFlagValuesFunc("dir", func() []string { + return base.GetFileList("") + }) + + cmd.MarkFlagRequired("dir") + + return cmd +} + +func printCmdIndex(curr *cobra.Command, indent int, prefix string) { + if curr.Name() == "help" { + return + } + fmt.Printf("%s* [%s](%s%s)\n", strings.Repeat(" ", indent), curr.Name(), prefix, "/"+curr.Name()) + for _, cmd := range curr.Commands() { + printCmdIndex(cmd, indent+1, prefix+"/"+curr.Name()) + } +} diff --git a/cmd/eip.go b/cmd/eip.go index f397cd3e0d..2e45dce86e 100644 --- a/cmd/eip.go +++ b/cmd/eip.go @@ -16,20 +16,22 @@ package cmd import ( "fmt" + "io" "net" "strconv" + "strings" "time" - "github.com/ucloud/ucloud-sdk-go/services/unet" - "github.com/spf13/cobra" + "github.com/ucloud/ucloud-sdk-go/services/unet" sdk "github.com/ucloud/ucloud-sdk-go/ucloud" - . "github.com/ucloud/ucloud-cli/base" + "github.com/ucloud/ucloud-cli/base" + "github.com/ucloud/ucloud-cli/model/status" ) -//NewCmdEIP ucloud eip +// NewCmdEIP ucloud eip func NewCmdEIP() *cobra.Command { var cmd = &cobra.Command{ Use: "eip", @@ -37,31 +39,37 @@ func NewCmdEIP() *cobra.Command { Long: `Manipulate EIP, such as list,allocate and release`, Args: cobra.NoArgs, } - cmd.AddCommand(NewCmdEIPList()) + out := base.Cxt.GetWriter() + cmd.AddCommand(NewCmdEIPList(out)) cmd.AddCommand(NewCmdEIPAllocate()) cmd.AddCommand(NewCmdEIPRelease()) cmd.AddCommand(NewCmdEIPBind()) cmd.AddCommand(NewCmdEIPUnbind()) + cmd.AddCommand(NewCmdEIPModifyBandwidth()) + cmd.AddCommand(NewCmdEIPSetChargeMode()) + cmd.AddCommand(NewCmdEIPJoinSharedBW()) + cmd.AddCommand(NewCmdEIPLeaveSharedBW()) return cmd } -//EIPRow 表格行 +// EIPRow 表格行 type EIPRow struct { Name string IP string ResourceID string Group string - Billing string + ChargeMode string Bandwidth string BindResource string Status string ExpirationTime string } -//NewCmdEIPList ucloud eip list -func NewCmdEIPList() *cobra.Command { - req := BizClient.NewDescribeEIPRequest() - fetchAll := sdk.Bool(false) +// NewCmdEIPList ucloud eip list +func NewCmdEIPList(out io.Writer) *cobra.Command { + req := base.BizClient.NewDescribeEIPRequest() + fetchAll := false + pageOff := false cmd := &cobra.Command{ Use: "list", Short: "List all EIP instances", @@ -69,54 +77,53 @@ func NewCmdEIPList() *cobra.Command { Example: "ucloud eip list", Run: func(cmd *cobra.Command, args []string) { var eipList []unet.UnetEIPSet - if *fetchAll == true { + if fetchAll || pageOff { list, err := fetchAllEip(*req.ProjectId, *req.Region) if err != nil { - HandleError(err) + base.HandleError(err) return } eipList = list } else { - resp, err := BizClient.DescribeEIP(req) + resp, err := base.BizClient.DescribeEIP(req) if err != nil { - HandleError(err) + base.HandleError(err) return } eipList = resp.EIPSet } - if global.json { - PrintJSON(eipList) - } else { - list := make([]EIPRow, 0) - for _, eip := range eipList { - row := EIPRow{} - row.Name = eip.Name - for _, ip := range eip.EIPAddr { - row.IP += ip.IP + " " + ip.OperatorName + " " - } - row.ResourceID = eip.EIPId - row.Group = eip.Tag - row.Billing = eip.PayMode - row.Bandwidth = strconv.Itoa(eip.Bandwidth) + "Mb" - if eip.Resource.ResourceId != "" { - row.BindResource = fmt.Sprintf("%s|%s(%s)", eip.Resource.ResourceName, eip.Resource.ResourceId, eip.Resource.ResourceType) - } - row.Status = eip.Status - row.ExpirationTime = time.Unix(int64(eip.ExpireTime), 0).Format("2006-01-02") - list = append(list, row) + list := make([]EIPRow, 0) + for _, eip := range eipList { + row := EIPRow{} + row.Name = eip.Name + for _, ip := range eip.EIPAddr { + row.IP += ip.IP + " " + ip.OperatorName + " " } - PrintTable(list, []string{"Name", "IP", "ResourceID", "Group", "Billing", "Bandwidth", "BindResource", "Status", "ExpirationTime"}) + row.ResourceID = eip.EIPId + row.Group = eip.Tag + row.ChargeMode = eip.PayMode + row.Bandwidth = strconv.Itoa(eip.Bandwidth) + "Mb" + if eip.Resource.ResourceID != "" { + row.BindResource = fmt.Sprintf("%s|%s(%s)", eip.Resource.ResourceName, eip.Resource.ResourceID, eip.Resource.ResourceType) + } + row.Status = eip.Status + row.ExpirationTime = time.Unix(int64(eip.ExpireTime), 0).Format("2006-01-02") + list = append(list, row) } + base.PrintList(list, out) }, } - req.ProjectId = cmd.Flags().String("project-id", ConfigInstance.ProjectID, "Assign project-id") - req.Region = cmd.Flags().String("region", ConfigInstance.Region, "Assign region") - req.Offset = cmd.Flags().Int("offset", 0, "Optional. Offset default 0") - req.Limit = cmd.Flags().Int("limit", 50, "Optional. Limit default 50, max value 100") - fetchAll = cmd.Flags().Bool("list-all", false, "List all eip") - cmd.Flags().SetFlagValues("list-all", "true", "false") + flags := cmd.Flags() + bindRegion(req, flags) + bindProjectID(req, flags) + req.Offset = flags.Int("offset", 0, "Optional. Offset default 0") + req.Limit = flags.Int("limit", 50, "Optional. Limit default 50, max value 100") + flags.BoolVar(&fetchAll, "list-all", false, "List all eip") + flags.BoolVar(&pageOff, "page-off", false, "Optional. Paging or not. Accept values: true or false") + flags.SetFlagValues("list-all", "true", "false") + flags.MarkDeprecated("list-all", "please use '--page-off' instead") return cmd } @@ -137,14 +144,14 @@ func getEIPIDbyIP(ip net.IP, projectID, region string) (string, error) { } func fetchAllEip(projectID, region string) ([]unet.UnetEIPSet, error) { - req := BizClient.NewDescribeEIPRequest() + req := base.BizClient.NewDescribeEIPRequest() list := []unet.UnetEIPSet{} req.ProjectId = sdk.String(projectID) req.Region = sdk.String(region) for offset, step := 0, 100; ; offset += step { req.Offset = &offset req.Limit = &step - resp, err := BizClient.DescribeEIP(req) + resp, err := base.BizClient.DescribeEIP(req) if err != nil { return nil, err } @@ -158,152 +165,487 @@ func fetchAllEip(projectID, region string) ([]unet.UnetEIPSet, error) { return list, nil } -//NewCmdEIPAllocate ucloud eip allocate +// states,paymodes 为nil时,不作为过滤条件 +func getAllEip(projectID, region string, states, paymodes []string) []string { + list, err := fetchAllEip(projectID, region) + if err != nil { + return nil + } + strs := []string{} + for _, item := range list { + rightState := false + if states == nil { + rightState = true + } else { + for _, s := range states { + if item.Status == s { + rightState = true + } + } + } + + rightPayMode := false + if paymodes == nil { + rightPayMode = true + } else { + for _, m := range paymodes { + if item.PayMode == m { + rightPayMode = true + } + } + } + if !rightPayMode || !rightState { + continue + } + + ips := []string{} + for _, ip := range item.EIPAddr { + ips = append(ips, ip.IP) + } + strs = append(strs, item.EIPId+"/"+strings.Join(ips, ",")) + } + return strs +} + +func getEIP(eipID string) (*unet.UnetEIPSet, error) { + req := base.BizClient.NewDescribeEIPRequest() + req.EIPIds = append(req.EIPIds, eipID) + resp, err := base.BizClient.DescribeEIP(req) + if err != nil { + return nil, err + } + if len(resp.EIPSet) == 1 { + return &resp.EIPSet[0], nil + } + return nil, fmt.Errorf("eip[%s] may not exist", eipID) +} + +// NewCmdEIPAllocate ucloud eip allocate func NewCmdEIPAllocate() *cobra.Command { var count *int - var req = BizClient.NewAllocateEIPRequest() + var req = base.BizClient.NewAllocateEIPRequest() var cmd = &cobra.Command{ Use: "allocate", Short: "Allocate EIP", Long: "Allocate EIP", - Example: "ucloud eip allocate --line Bgp --bandwidth 2", + Example: "ucloud eip allocate --line BGP --bandwidth-mb 2", Run: func(cmd *cobra.Command, args []string) { - if *req.OperatorName == "BGP" { - *req.OperatorName = "Bgp" + if *req.OperatorName == "" { + *req.OperatorName = getEIPLine(*req.Region) } for i := 0; i < *count; i++ { - resp, err := BizClient.AllocateEIP(req) + resp, err := base.BizClient.AllocateEIP(req) if err != nil { - HandleError(err) - } else { - for _, eip := range resp.EIPSet { - Cxt.Printf("allocate EIP[%s] ", eip.EIPId) - for _, ip := range eip.EIPAddr { - Cxt.Printf("IP:%s Line:%s \n", ip.IP, ip.OperatorName) - } + base.HandleError(err) + continue + } + for _, eip := range resp.EIPSet { + base.Cxt.Printf("allocate EIP[%s] ", eip.EIPId) + for _, ip := range eip.EIPAddr { + base.Cxt.Printf("IP:%s Line:%s \n", ip.IP, ip.OperatorName) } } } }, } cmd.Flags().SortFlags = false - req.OperatorName = cmd.Flags().String("line", "", "Required. 'BGP' or 'International'. 'BGP' could be set in China mainland regions, such as cn-bj2 etc. 'International' could be set in the regions beyond mainland, such as hk, tw-kh, us-ws etc.") req.Bandwidth = cmd.Flags().Int("bandwidth-mb", 0, "Required. Bandwidth(Unit:Mbps).The range of value related to network charge mode. By traffic [1, 200]; by bandwidth [1,800] (Unit: Mbps); it could be 0 if the eip belong to the shared bandwidth") - req.ProjectId = cmd.Flags().String("project-id", ConfigInstance.ProjectID, "Optional. Assign project-id") - req.Region = cmd.Flags().String("region", ConfigInstance.Region, "Optional. Assign region") - req.PayMode = cmd.Flags().String("charge-mode", "Bandwidth", "Optional. charge-mode is an enumeration value. 'Traffic','Bandwidth' or 'ShareBandwidth'") - req.ShareBandwidthId = cmd.Flags().String("share-bandwidth-id", "", "Optional. ShareBandwidthId, required only when charge-mode is 'ShareBandwidth'") + req.OperatorName = cmd.Flags().String("line", "", "Optional. 'BGP' or 'International'. 'BGP' could be set in China mainland regions, such as cn-bj2 etc. 'International' could be set in the regions beyond mainland, such as hk, tw-kh, us-ws etc.") + bindProjectID(req, cmd.Flags()) + bindRegion(req, cmd.Flags()) + req.PayMode = cmd.Flags().String("traffic-mode", "Bandwidth", "Optional. traffic-mode is an enumeration value. 'Traffic','Bandwidth' or 'ShareBandwidth'") + req.ShareBandwidthId = cmd.Flags().String("share-bandwidth-id", "", "Optional. ShareBandwidthId, required only when traffic-mode is 'ShareBandwidth'") req.Quantity = cmd.Flags().Int("quantity", 1, "Optional. The duration of the instance. N years/months.") req.ChargeType = cmd.Flags().String("charge-type", "Month", "Optional. Enumeration value.'Year',pay yearly;'Month',pay monthly;'Dynamic', pay hourly(requires permission),'Trial', free trial(need permission)") req.Tag = cmd.Flags().String("group", "Default", "Optional. Group of your EIP.") req.Name = cmd.Flags().String("name", "EIP", "Optional. Name of your EIP.") req.Remark = cmd.Flags().String("remark", "", "Optional. Remark of your EIP.") - req.CouponId = cmd.Flags().String("coupon-id", "", "Optional. Coupon ID, The Coupon can deducte part of the payment") count = cmd.Flags().Int("count", 1, "Optional. Count of EIP to allocate") cmd.Flags().SetFlagValues("line", "BGP", "International") - cmd.Flags().SetFlagValues("charge-mode", "Bandwidth", "Traffic", "ShareBandwidth") + cmd.Flags().SetFlagValues("traffic-mode", "Bandwidth", "Traffic", "ShareBandwidth") cmd.Flags().SetFlagValues("charge-type", "Month", "Year", "Dynamic", "Trial") - cmd.MarkFlagRequired("line") cmd.MarkFlagRequired("bandwidth-mb") return cmd } -//NewCmdEIPBind ucloud eip bind +// NewCmdEIPBind ucloud eip bind func NewCmdEIPBind() *cobra.Command { - var projectID, region, eipID, resourceID, resourceType *string + var projectID, region, resourceID, resourceType *string + var eipIDs []string cmd := &cobra.Command{ Use: "bind", Short: "Bind EIP with uhost", Long: "Bind EIP with uhost", Example: "ucloud eip bind --eip-id eip-xxx --resource-id uhost-xxx", Run: func(cmd *cobra.Command, args []string) { - bindEIP(resourceID, resourceType, eipID, projectID, region) + for _, eipID := range eipIDs { + bindEIP(resourceID, resourceType, &eipID, projectID, region) + } }, } - cmd.Flags().SortFlags = false - eipID = cmd.Flags().String("eip-id", "", "Required. EIPId to bind") + flags := cmd.Flags() + flags.SortFlags = false + + cmd.Flags().StringSliceVar(&eipIDs, "eip-id", nil, "Required. EIPId to bind") resourceID = cmd.Flags().String("resource-id", "", "Required. ResourceID , which is the UHostId of uhost") resourceType = cmd.Flags().String("resource-type", "uhost", "Requried. ResourceType, type of resource to bind with eip. 'uhost','vrouter','ulb','upm','hadoophost'.eg..") - projectID = cmd.Flags().String("project-id", ConfigInstance.ProjectID, "Optional. Assign project-id") - region = cmd.Flags().String("region", ConfigInstance.Region, "Optional. Assign region") + projectID = cmd.Flags().String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + region = cmd.Flags().String("region", base.ConfigIns.Region, "Optional. Assign region") + + cmd.Flags().SetFlagValues("resource-type", "uhost", "vrouter", "ulb", "upm", "hadoophost", "fortresshost", "udockhost", "udhost", "natgw", "udb", "vpngw", "ucdr", "dbaudit") + cmd.Flags().SetFlagValuesFunc("eip-id", func() []string { + return getAllEip(*projectID, *region, []string{status.EIP_FREE}, nil) + }) + cmd.MarkFlagRequired("eip-id") cmd.MarkFlagRequired("resource-id") - cmd.Flags().SetFlagValues("resource-type", "uhost", "vrouter", "ulb", "upm", "hadoophost", "fortresshost", "udockhost", "udhost", "natgw", "udb", "vpngw", "ucdr", "dbaudit") + return cmd } func bindEIP(resourceID, resourceType, eipID, projectID, region *string) { - req := BizClient.NewBindEIPRequest() + ip := net.ParseIP(*eipID) + if ip != nil { + id, err := getEIPIDbyIP(ip, *projectID, *region) + if err != nil { + base.HandleError(err) + } else { + *eipID = id + } + } + req := base.BizClient.NewBindEIPRequest() req.ResourceId = resourceID req.ResourceType = resourceType - req.EIPId = eipID - req.ProjectId = projectID + req.EIPId = sdk.String(base.PickResourceID(*eipID)) + req.ProjectId = sdk.String(base.PickResourceID(*projectID)) req.Region = region - _, err := BizClient.BindEIP(req) + _, err := base.BizClient.BindEIP(req) if err != nil { - HandleError(err) + base.HandleError(err) } else { - Cxt.Printf("bind EIP[%s] with %s[%s]\n", *req.EIPId, *req.ResourceType, *req.ResourceId) + base.Cxt.Printf("bind EIP[%s] with %s[%s]\n", *req.EIPId, *req.ResourceType, *req.ResourceId) } } -//NewCmdEIPUnbind ucloud eip unbind -func NewCmdEIPUnbind() *cobra.Command { +func sbindEIP(resourceID, resourceType, eipID, projectID, region *string) ([]string, error) { + logs := make([]string, 0) + ip := net.ParseIP(*eipID) + if ip != nil { + id, err := getEIPIDbyIP(ip, *projectID, *region) + if err != nil { + base.HandleError(err) + } else { + *eipID = id + } + } + req := base.BizClient.NewBindEIPRequest() + req.ResourceId = resourceID + req.ResourceType = resourceType + req.EIPId = sdk.String(base.PickResourceID(*eipID)) + req.ProjectId = sdk.String(base.PickResourceID(*projectID)) + req.Region = region + logs = append(logs, fmt.Sprintf("api: BindEIP, request: %v", base.ToQueryMap(req))) + _, err := base.BizClient.BindEIP(req) + if err != nil { + logs = append(logs, fmt.Sprintf("bind eip failed: %v", err)) + return logs, err + } + logs = append(logs, fmt.Sprintf("bind eip[%s] with %s[%s] successfully", *req.EIPId, *req.ResourceType, *req.ResourceId)) + return logs, nil +} - var req = BizClient.NewUnBindEIPRequest() - var cmd = &cobra.Command{ +// NewCmdEIPUnbind ucloud eip unbind +func NewCmdEIPUnbind() *cobra.Command { + eipIDs := []string{} + req := base.BizClient.NewUnBindEIPRequest() + cmd := &cobra.Command{ Use: "unbind", Short: "Unbind EIP with uhost", Long: "Unbind EIP with uhost", - Example: "ucloud eip unbind --eip-id eip-xxx --resource-id uhost-xxx", + Example: "ucloud eip unbind --eip-id eip-xxx", Run: func(cmd *cobra.Command, args []string) { - req.ResourceType = sdk.String("uhost") - _, err := BizClient.UnBindEIP(req) - if err != nil { - HandleError(err) - } else { - Cxt.Printf("unbind EIP[%s] with %s[%s]\n", *req.EIPId, *req.ResourceType, *req.ResourceId) + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + for _, eip := range eipIDs { + eipIns, err := getEIP(base.PickResourceID(eip)) + if err != nil { + base.HandleError(err) + return + } + req.EIPId = sdk.String(base.PickResourceID(eip)) + req.ResourceId = sdk.String(eipIns.Resource.ResourceID) + req.ResourceType = sdk.String(eipIns.Resource.ResourceType) + _, err = base.BizClient.UnBindEIP(req) + if err != nil { + base.HandleError(err) + return + } + base.Cxt.Printf("unbind EIP[%s] with %s[%s]\n", *req.EIPId, *req.ResourceType, *req.ResourceId) } }, } - cmd.Flags().SortFlags = false - req.EIPId = cmd.Flags().String("eip-id", "", "Required. EIPId to unbind") - req.ResourceId = cmd.Flags().String("resource-id", "", "Required. ResourceID , which is the UHostId of uhost") - req.ProjectId = cmd.Flags().String("project-id", ConfigInstance.ProjectID, "Optional. Assign project-id") - req.Region = cmd.Flags().String("region", ConfigInstance.Region, "Optional. Assign region") + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&eipIDs, "eip-id", nil, "Required. Resource ID of eips to unbind with some resource") + bindRegion(req, flags) + bindProjectID(req, flags) + cmd.MarkFlagRequired("eip-id") - cmd.MarkFlagRequired("resource-id") + cmd.Flags().SetFlagValuesFunc("eip-id", func() []string { + return getAllEip(*req.ProjectId, *req.Region, []string{status.EIP_USED}, nil) + }) return cmd } -//NewCmdEIPRelease ucloud eip release +func unbindEIP(resourceID, resourceType, eipID, projectID, region string) ([]string, error) { + logs := make([]string, 0) + eipID = base.PickResourceID(eipID) + ip := net.ParseIP(eipID) + if ip != nil { + id, err := getEIPIDbyIP(ip, projectID, region) + if err != nil { + base.HandleError(err) + } else { + eipID = id + } + } + req := base.BizClient.NewUnBindEIPRequest() + req.ResourceId = &resourceID + req.ResourceType = &resourceType + req.EIPId = &eipID + req.ProjectId = sdk.String(base.PickResourceID(projectID)) + req.Region = ®ion + logs = append(logs, fmt.Sprintf("api: UnBindEIP, request: %v", base.ToQueryMap(req))) + _, err := base.BizClient.UnBindEIP(req) + if err != nil { + logs = append(logs, fmt.Sprintf("unbind eip failed: %v", err)) + return logs, err + } + logs = append(logs, fmt.Sprintf("unbind eip[%s] with %s[%s] successfully", *req.EIPId, *req.ResourceType, *req.ResourceId)) + return logs, nil +} + +// NewCmdEIPRelease ucloud eip release func NewCmdEIPRelease() *cobra.Command { var ids []string - var req = BizClient.NewReleaseEIPRequest() - var cmd = &cobra.Command{ + req := base.BizClient.NewReleaseEIPRequest() + cmd := &cobra.Command{ Use: "release", Short: "Release EIP", Long: "Release EIP", Example: "ucloud eip release --eip-id eip-xx1,eip-xx2", + Run: func(cmd *cobra.Command, args []string) { + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + for _, id := range ids { + req.EIPId = sdk.String(base.PickResourceID(id)) + _, err := base.BizClient.ReleaseEIP(req) + if err != nil { + base.HandleError(err) + } else { + base.Cxt.Printf("eip[%s] released\n", *req.EIPId) + } + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + flags.StringSliceVarP(&ids, "eip-id", "", nil, "Required. Resource ID of the EIPs you want to release") + bindProjectID(req, flags) + bindRegion(req, flags) + cmd.MarkFlagRequired("eip-id") + flags.SetFlagValuesFunc("eip-id", func() []string { + return getAllEip(*req.ProjectId, *req.Region, []string{status.EIP_FREE}, nil) + }) + + return cmd +} + +// NewCmdEIPModifyBandwidth ucloud eip modify-bw +func NewCmdEIPModifyBandwidth() *cobra.Command { + ids := []string{} + req := base.BizClient.NewModifyEIPBandwidthRequest() + cmd := &cobra.Command{ + Use: "modify-bw", + Short: "Modify bandwith of EIP instances", + Long: "Modify bandwith of EIP instances", + Example: "ucloud eip modify-bw --eip-id eip-xx1,eip-xx2 --bandwidth-mb 20", + // Deprecated: "use 'ucloud eip modiy'", + Run: func(cmd *cobra.Command, args []string) { + for _, id := range ids { + id = base.PickResourceID(id) + req.EIPId = &id + _, err := base.BizClient.ModifyEIPBandwidth(req) + if err != nil { + base.HandleError(err) + } else { + base.Cxt.Printf("eip[%s]'s bandwidth modified\n", id) + } + } + }, + } + cmd.Flags().SortFlags = false + cmd.Flags().StringSliceVarP(&ids, "eip-id", "", nil, "Required, Resource ID of EIPs to modify bandwidth") + req.Bandwidth = cmd.Flags().Int("bandwidth-mb", 0, "Required. Bandwidth of EIP after modifed. Charge by traffic, range [1,300]; charge by bandwidth, range [1,800]") + req.ProjectId = cmd.Flags().String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + req.Region = cmd.Flags().String("region", base.ConfigIns.Region, "Optional. Assign region") + cmd.Flags().SetFlagValuesFunc("eip-id", func() []string { + return getAllEip(*req.ProjectId, *req.Region, nil, nil) + }) + cmd.MarkFlagRequired("eip-id") + cmd.MarkFlagRequired("bandwidth-mb") + return cmd +} + +// NewCmdEIPSetChargeMode ucloud eip modify-traffic-mode +func NewCmdEIPSetChargeMode() *cobra.Command { + ids := []string{} + req := base.BizClient.NewSetEIPPayModeRequest() + cmd := &cobra.Command{ + Use: "modify-traffic-mode", + Short: "Modify charge mode of EIP instances", + Long: "Modify charge mode of EIP instances", + Example: "ucloud eip modify-traffic-mode --eip-id eip-xx1,eip-xx2 --traffic-mode Traffic", Run: func(cmd *cobra.Command, args []string) { for _, id := range ids { + id = base.PickResourceID(id) req.EIPId = &id - _, err := BizClient.ReleaseEIP(req) + eipIns, err := getEIP(id) if err != nil { - HandleError(err) + base.HandleError(err) + return + } + req.Bandwidth = sdk.Int(eipIns.Bandwidth) + _, err = base.BizClient.SetEIPPayMode(req) + if err != nil { + base.HandleError(err) } else { - Cxt.Printf("released EIP[%v]\n", *req.EIPId) + base.Cxt.Printf("eip[%s]'s charge mode was modified to %s\n", id, *req.PayMode) } } }, } + cmd.Flags().SortFlags = false - cmd.Flags().StringSliceVarP(&ids, "eip-id", "", make([]string, 0), "Required. EIPIds of the EIP you want to release") - req.ProjectId = cmd.Flags().String("project-id", ConfigInstance.ProjectID, "Optional. Assign project-id") - req.Region = cmd.Flags().String("region", ConfigInstance.Region, "Optional. Assign region") + cmd.Flags().StringSliceVarP(&ids, "eip-id", "", nil, "Required, Resource ID of EIPs to modify charge mode") + req.PayMode = cmd.Flags().String("traffic-mode", "", "Required, Charge mode of eip, 'Traffic','Bandwidth' or 'PostAccurateBandwidth'") + req.ProjectId = cmd.Flags().String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + req.Region = cmd.Flags().String("region", base.ConfigIns.Region, "Optional. Assign region") + cmd.Flags().SetFlagValues("traffic-mode", "Bandwidth", "Traffic", "PostAccurateBandwidth") + cmd.Flags().SetFlagValuesFunc("eip-id", func() []string { + return getAllEip(*req.ProjectId, *req.Region, nil, nil) + }) + cmd.MarkFlagRequired("eip-id") + cmd.MarkFlagRequired("traffic-mode") + return cmd +} + +// NewCmdEIPJoinSharedBW ucloud eip join-shared-bw +func NewCmdEIPJoinSharedBW() *cobra.Command { + eipIDs := []string{} + req := base.BizClient.NewAssociateEIPWithShareBandwidthRequest() + cmd := &cobra.Command{ + Use: "join-shared-bw", + Short: "Join shared bandwidth", + Long: "Join shared bandwidth", + Example: "ucloud eip join-shared-bw --eip-id eip-xxx --shared-bw-id bwshare-xxx", + Run: func(c *cobra.Command, args []string) { + for _, eip := range eipIDs { + req.EIPIds = append(req.EIPIds, base.PickResourceID(eip)) + } + req.ShareBandwidthId = sdk.String(base.PickResourceID(*req.ShareBandwidthId)) + _, err := base.BizClient.AssociateEIPWithShareBandwidth(req) + if err != nil { + base.HandleError(err) + return + } + base.Cxt.Printf("eip%v joined shared bandwidth[%s]\n", req.EIPIds, *req.ShareBandwidthId) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + flags.StringSliceVar(&eipIDs, "eip-id", nil, "Required. Resource ID of EIPs to join shared bandwdith") + req.ShareBandwidthId = flags.String("shared-bw-id", "", "Required. Resource ID of shared bandwidth to be joined") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Region, see 'ucloud region'") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Project-id, see 'ucloud project list'") + flags.SetFlagValuesFunc("eip-id", func() []string { + return getAllEip(*req.ProjectId, *req.Region, nil, []string{status.EIP_CHARGE_BANDWIDTH, status.EIP_CHARGE_TRAFFIC}) + }) + flags.SetFlagValuesFunc("shared-bw-id", func() []string { + list, _ := getAllSharedBW(*req.ProjectId, *req.Region) + return list + }) cmd.MarkFlagRequired("eip-id") + cmd.MarkFlagRequired("shared-bw-id") + + return cmd +} + +// NewCmdEIPLeaveSharedBW ucloud eip leave-shared-bw +func NewCmdEIPLeaveSharedBW() *cobra.Command { + eipIDs := []string{} + req := base.BizClient.NewDisassociateEIPWithShareBandwidthRequest() + cmd := &cobra.Command{ + Use: "leave-shared-bw", + Short: "Leave shared bandwidth", + Long: "Leave shared bandwidth", + Example: "ucloud eip leave-shared-bw --eip-id eip-b2gvu3", + Run: func(c *cobra.Command, args []string) { + if *req.ShareBandwidthId == "" { + for _, eipID := range eipIDs { + eipIns, err := getEIP(base.PickResourceID(eipID)) + if err != nil { + base.HandleError(err) + continue + } + sharedBWID := eipIns.ShareBandwidthSet.ShareBandwidthId + if sharedBWID == "" { + base.Cxt.Printf("eip[%s] doesn't join any shared bandwidth\n", eipID) + continue + } + req.ShareBandwidthId = sdk.String(sharedBWID) + req.EIPIds = []string{base.PickResourceID(eipID)} + _, err = base.BizClient.DisassociateEIPWithShareBandwidth(req) + if err != nil { + base.HandleError(err) + continue + } + base.Cxt.Printf("eip[%s] left shared bandwidth[%s]\n", eipID, sharedBWID) + } + } else { + for _, id := range eipIDs { + req.EIPIds = append(req.EIPIds, base.PickResourceID(id)) + } + *req.ShareBandwidthId = base.PickResourceID(*req.ShareBandwidthId) + _, err := base.BizClient.DisassociateEIPWithShareBandwidth(req) + if err != nil { + base.HandleError(err) + return + } + base.Cxt.Printf("eip%v left shared bandwidth[%s]\n", eipIDs, *req.ShareBandwidthId) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + flags.StringSliceVar(&eipIDs, "eip-id", nil, "Required. Resource ID of EIPs to leave shared bandwidth") + req.Bandwidth = flags.Int("bandwidth-mb", 1, "Required. Bandwidth of EIP after leaving shared bandwidth, ranging [1,300] for 'Traffic' charge mode, ranging [1,800] for 'Bandwidth' charge mode. Unit:Mb") + req.PayMode = flags.String("traffic-mode", "Bandwidth", "Optional. Charge mode of the EIP after leaving shared bandwidth, 'Bandwidth' or 'Traffic'") + req.ShareBandwidthId = flags.String("shared-bw-id", "", "Optional. Resource ID of shared bandwidth instance, assign this flag to make the operation faster") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Region, see 'ucloud region'") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Project-id, see 'ucloud project list'") + + flags.SetFlagValues("traffic-mode", "Bandwidth", "Traffic") + flags.SetFlagValuesFunc("eip-id", func() []string { + return getAllEip(*req.ProjectId, *req.Region, nil, []string{status.EIP_CHARGE_SHARE}) + }) + flags.SetFlagValuesFunc("shared-bw-id", func() []string { + list, _ := getAllSharedBW(*req.ProjectId, *req.Region) + return list + }) + cmd.MarkFlagRequired("bandwidth") + cmd.MarkFlagRequired("eip-id") return cmd } diff --git a/cmd/ext.go b/cmd/ext.go new file mode 100644 index 0000000000..d50850ec3c --- /dev/null +++ b/cmd/ext.go @@ -0,0 +1,205 @@ +// Copyright © 2018 NAME HERE tony.li@ucloud.cn +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + + "github.com/ucloud/ucloud-cli/base" + "github.com/ucloud/ucloud-cli/model/status" + "github.com/ucloud/ucloud-sdk-go/services/uhost" + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" +) + +// NewCmdExt ucloud ext +func NewCmdExt() *cobra.Command { + cmd := &cobra.Command{ + Use: "ext", + Short: "extended commands of UCloud CLI", + Long: "extended commands of UCloud CLI", + } + cmd.AddCommand(NewCmdExtUHost()) + return cmd +} + +// NewCmdExtUHost ucloud ext uhost +func NewCmdExtUHost() *cobra.Command { + cmd := &cobra.Command{ + Use: "uhost", + Short: "extended uhost commands", + Long: "extended uhost commands", + } + cmd.AddCommand(NewCmdExtUHostSwitchEIP()) + return cmd +} + +// NewCmdExtUHostSwitchEIP ucloud ext uhost switch-eip +func NewCmdExtUHostSwitchEIP() *cobra.Command { + var project, region, zone, chargeType, trafficMode, shareBandwidthID string + var uhostIDs, eipAddrs []string + var eipBandwidth, quntity int + var unbind, release bool + + cmd := &cobra.Command{ + Use: "switch-eip", + Short: "Switch EIP for UHost instances", + Long: "Switch EIP for UHost instances", + Example: "ucloud ext uhost switch-eip --uhost-id uhost-1n1sxx2,uhost-li4jxx1 --create-eip-bandwidth-mb 2", + Run: func(c *cobra.Command, args []string) { + project = base.PickResourceID(project) + eipAddrMap := make(map[string]bool) + for _, addr := range eipAddrs { + eipAddrMap[addr] = true + } + logs := make([]string, 0) + for _, idname := range uhostIDs { + uhostID := base.PickResourceID(idname) + logs = append(logs, fmt.Sprintf("describe uhost instance by uhostID %s", uhostID)) + ins, err := describeUHostByID(uhostID, project, region, zone) + if err != nil { + errStr := fmt.Sprintf("describe uhost %s failed: %v", uhostID, err) + base.HandleError(fmt.Errorf(errStr)) + logs = append(logs, errStr) + continue + } + uhostIns, ok := ins.(*uhost.UHostInstanceSet) + if !ok { + errStr := fmt.Sprintf("uhost %s does not exist", uhostID) + base.HandleError(fmt.Errorf(errStr)) + logs = append(logs, errStr) + continue + } + for _, ip := range uhostIns.IPSet { + if ip.IPId == "" { + continue + } + if len(eipAddrs) > 0 && eipAddrMap[ip.IP] == false { + continue + } + //申请EIP + req := base.BizClient.NewAllocateEIPRequest() + req.Region = ®ion + req.ProjectId = &project + if strings.HasPrefix(region, "cn") { + req.OperatorName = sdk.String("BGP") + } else { + req.OperatorName = sdk.String("International") + } + req.Bandwidth = &eipBandwidth + req.ChargeType = &chargeType + req.Quantity = &quntity + req.PayMode = &trafficMode + if trafficMode == "ShareBandwidth" { + if shareBandwidthID != "" { + req.ShareBandwidthId = &shareBandwidthID + } else { + errStr := "create-eip-share-bandwidth-id should not be empty when create-eip-traffic-mode is assigned 'ShareBandwidth'" + logs = append(logs, errStr) + base.HandleError(fmt.Errorf(errStr)) + return + } + } + logs = append(logs, fmt.Sprintf("api AllocateEIP, request:%v", base.ToQueryMap(req))) + resp, err := base.BizClient.AllocateEIP(req) + if err != nil { + errStr := fmt.Sprintf("allocate EIP failed: %v", err) + logs = append(logs, errStr) + base.HandleError(fmt.Errorf(errStr)) + continue + } + if len(resp.EIPSet) != 1 { + errStr := fmt.Sprintf("allocate EIP failed, length of eip set is not 1") + base.HandleError(fmt.Errorf(errStr)) + logs = append(logs, errStr) + continue + } + eipID := resp.EIPSet[0].EIPId + eipRet := fmt.Sprintf("allocated new eip %s|%s", eipID, resp.EIPSet[0].EIPAddr[0].IP) + logs = append(logs, eipRet) + fmt.Println(eipRet) + + //绑定新EIP + slogs, err2 := sbindEIP(&uhostID, sdk.String("uhost"), &eipID, &project, ®ion) + logs = append(logs, slogs...) + if err2 != nil { + base.HandleError(fmt.Errorf("bind new eip %s failed: %v", eipID, err2)) + continue + } + fmt.Printf("bound eip %s with uhost %s\n", eipID, uhostID) + + if unbind { + slogs, err := unbindEIP(uhostID, "uhost", ip.IPId, project, region) + logs = append(logs, slogs...) + if err != nil { + base.HandleError(fmt.Errorf("unbind eip %s failed: %v", ip.IPId, err)) + continue + } + fmt.Printf("unbound eip %s|%s with uhost %s\n", ip.IPId, ip.IP, uhostID) + } + + if release { + req := base.BizClient.NewReleaseEIPRequest() + req.ProjectId = &project + req.Region = ®ion + req.EIPId = sdk.String(ip.IPId) + logs = append(logs, fmt.Sprintf("api ReleaseEIP, request:%v", base.ToQueryMap(req))) + _, err := base.BizClient.ReleaseEIP(req) + if err != nil { + errStr := fmt.Sprintf("release eip %s failed: %v", ip.IPId, err) + logs = append(logs, errStr) + base.HandleError(fmt.Errorf(errStr)) + continue + } + releaseRet := fmt.Sprintf("released eip %s|%s", ip.IPId, ip.IP) + logs = append(logs, releaseRet) + fmt.Println(releaseRet) + } + base.LogInfo(logs...) + } + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&uhostIDs, "uhost-id", nil, "Required. Resource ID of uhost instances to switch EIP") + flags.StringSliceVar(&eipAddrs, "eip-addr", nil, "Optional. Address of EIP instances to be replaced. if eip-id is empty, replace all of the EIPs bound with the uhost ") + flags.BoolVar(&unbind, "unbind-all", true, "Optional. Unbind all EIP instances that has been replaced. Accept values:true or false") + flags.BoolVar(&release, "release-all", true, "Optional. Release all EIP instances that has been replaced. Accept values:true or false") + flags.IntVar(&eipBandwidth, "create-eip-bandwidth-mb", 1, "Optional. Bandwidth of EIP instance to be create with. Unit:Mb") + flags.StringVar(&trafficMode, "create-eip-traffic-mode", "Bandwidth", "Optional. traffic-mode is an enumeration value. 'Traffic','Bandwidth' or 'ShareBandwidth'") + flags.StringVar(&shareBandwidthID, "create-eip-share-bandwidth-id", "", "Optional. ShareBandwidthId, required only when traffic-mode is 'ShareBandwidth'") + flags.StringVar(&chargeType, "create-eip-charge-type", "Month", "Optional. Enumeration value.'Year',pay yearly;'Month',pay monthly;'Dynamic', pay hourly") + flags.IntVar(&quntity, "create-eip-quantity", 1, "Optional. The duration of the instance. N years/months.") + + flags.SetFlagValues("create-eip-traffic-mode", "Bandwidth", "Traffic", "ShareBandwidth") + flags.SetFlagValues("create-eip-charge-type", "Month", "Year", "Dynamic", "Trial") + + bindProjectIDS(&project, flags) + bindRegionS(®ion, flags) + bindZoneEmptyS(&zone, ®ion, flags) + + flags.SetFlagValuesFunc("uhost-id", func() []string { + return getUhostList([]string{status.HOST_RUNNING, status.HOST_STOPPED, status.HOST_FAIL}, project, region, zone) + }) + + cmd.MarkFlagRequired("uhost-id") + + return cmd +} diff --git a/cmd/firewall.go b/cmd/firewall.go new file mode 100644 index 0000000000..a1f8deb57f --- /dev/null +++ b/cmd/firewall.go @@ -0,0 +1,613 @@ +// Copyright © 2018 NAME HERE tony.li@ucloud.cn +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "bufio" + "fmt" + "io" + "os" + "strings" + + "github.com/spf13/cobra" + + "github.com/ucloud/ucloud-sdk-go/services/unet" + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + + "github.com/ucloud/ucloud-cli/base" +) + +// NewCmdFirewall ucloud firewall +func NewCmdFirewall() *cobra.Command { + cmd := &cobra.Command{ + Use: "firewall", + Short: "List and manipulate extranet firewall", + Long: `List and manipulate extranet firewall`, + Args: cobra.NoArgs, + } + writer := base.Cxt.GetWriter() + cmd.AddCommand(NewCmdFirewallList(writer)) + cmd.AddCommand(NewCmdFirewallCreate(writer)) + cmd.AddCommand(NewCmdFirewallAddRule(writer)) + cmd.AddCommand(NewCmdFirewallDeleteRule(writer)) + cmd.AddCommand(NewCmdFirewallApply()) + cmd.AddCommand(NewCmdFirewallCopy()) + cmd.AddCommand(NewCmdFirewallDelete()) + cmd.AddCommand(NewCmdFirewallResource(writer)) + cmd.AddCommand(NewCmdFirewallUpdate(writer)) + + return cmd +} + +// FirewallRow 表格行 +type FirewallRow struct { + ResourceID string + FirewallName string + Rule string + Group string + RuleAmount int + BoundResourceAmount int + CreationTime string +} + +// NewCmdFirewallList ucloud firewall list +func NewCmdFirewallList(out io.Writer) *cobra.Command { + req := base.BizClient.NewDescribeFirewallRequest() + cmd := &cobra.Command{ + Use: "list", + Short: "List extranet firewall", + Long: `List extranet firewall`, + Run: func(cmd *cobra.Command, args []string) { + resp, err := base.BizClient.DescribeFirewall(req) + if err != nil { + base.HandleError(err) + return + } + list := []FirewallRow{} + for _, fw := range resp.DataSet { + row := FirewallRow{} + row.ResourceID = fw.FWId + row.FirewallName = fw.Name + row.Group = fw.Tag + row.RuleAmount = len(fw.Rule) + row.BoundResourceAmount = fw.ResourceCount + row.CreationTime = base.FormatDate(fw.CreateTime) + if fw.Remark != "" { + row.FirewallName += "\nremark:" + fw.Remark + "\n" + } + for _, r := range fw.Rule { + rule := fmt.Sprintf("%s|%s|%s|%s|%s", r.ProtocolType, r.DstPort, r.SrcIP, r.RuleAction, r.Priority) + row.Rule += rule + "\n" + } + list = append(list, row) + } + base.PrintList(list, out) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Region, see 'ucloud region'") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Project-id, see 'ucloud project list'") + req.FWId = flags.String("firewall-id", "", "Optional. The Rsource ID of firewall. Return all firewalls by default.") + req.ResourceType = flags.String("bound-resource-type", "", "Optional. The type of resource bound on the firewall") + req.ResourceId = flags.String("bound-resource-id", "", "Optional. The resource ID of resource bound on the firewall") + req.Offset = flags.Int("offset", 0, "Optional. Offset") + req.Limit = flags.Int("limit", 50, "Optional. Limit") + return cmd +} + +func parseRulesFromFile(filePath string) ([]string, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + lines := []string{} + scanner := bufio.NewScanner(file) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + if err := scanner.Err(); err != nil { + return nil, err + } + return lines, nil +} + +// NewCmdFirewallCreate ucloud firewall create +func NewCmdFirewallCreate(out io.Writer) *cobra.Command { + var rulesFilePath string + var rules []string + + req := base.BizClient.NewCreateFirewallRequest() + cmd := &cobra.Command{ + Use: "create", + Short: "Create firewall", + Long: "Create firewall", + Example: `ucloud firewall create --name test3 --rules "TCP|22|0.0.0.0/0|ACCEPT|HIGH" --rules-file firewall_rules.txt`, + Run: func(c *cobra.Command, args []string) { + if rules == nil && rulesFilePath == "" { + fmt.Fprintln(out, "Error: flags rules and rules-file can't be both empty") + return + } + if rulesFilePath != "" { + lines, err := parseRulesFromFile(rulesFilePath) + if err != nil { + base.HandleError(err) + return + } + rules = append(rules, lines...) + } + req.Rule = rules + resp, err := base.BizClient.CreateFirewall(req) + if err != nil { + base.HandleError(err) + return + } + base.Cxt.Printf("firewall[%s] created\n", resp.FWId) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + flags.StringSliceVar(&rules, "rules", nil, "Required if rules-file doesn't exist. Schema: Protocol|Port|IP|Action|Level. Prototol range 'TCP','UDP','ICMP' and 'GRE'; Port is a local port accessed by source address, port range [0-65535]; IP is the source address of the network packet that requests ucloud host resource, supporting IP address and network segment, such as '120.132.69.216' or '0.0.0.0/0'; Action is the processing behavior of the packet when the firewall is in effect, including 'ACCEPT' AND 'DROP'; Level, when a rule is added to a firewall, the rules take effect in order of level, which range 'HIGH','MEDIUM' and 'LOW'. For example, 'TCP|22|192.168.1.1/22|DROP|LOW'") + flags.StringVar(&rulesFilePath, "rules-file", "", "Required if rules doesn't exist. Path of rules file, in which each rule occupies one line. Schema: Protocol|Port|IP|Action|Level.") + req.Name = flags.String("name", "", "Required. Name of firewall to create") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Region, see 'ucloud region'") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Project-id, see 'ucloud project list'") + req.Tag = flags.String("group", "", "Optional. Group of the firewall to create") + req.Remark = flags.String("remark", "", "Optional. Remark of the firewall to create") + cmd.MarkFlagRequired("name") + flags.SetFlagValuesFunc("rules-file", func() []string { + return base.GetFileList("") + }) + return cmd +} + +// NewCmdFirewallAddRule ucloud firewall add-rule +func NewCmdFirewallAddRule(out io.Writer) *cobra.Command { + var rulesFilePath string + var fwIDs []string + req := base.BizClient.NewUpdateFirewallRequest() + cmd := &cobra.Command{ + Use: "add-rule", + Short: "Add rule to firewall instance", + Long: "Add rule to firewall instance", + Example: `ucloud firewall add-rule --fw-id firewall-2xxxxz/test.lxj2 --rules "TCP|24|0.0.0.0/0|ACCEPT|HIGH" --rules-file firewall_rules.txt`, + Run: func(c *cobra.Command, args []string) { + if req.Rule == nil && rulesFilePath == "" { + fmt.Fprintln(out, "Error: flags rules and rules-file can't be both empty") + return + } + for _, fwID := range fwIDs { + id := base.PickResourceID(fwID) + req.FWId = &id + firewall, err := getFirewall(*req.FWId, *req.ProjectId, *req.Region) + if err != nil { + base.HandleError(err) + return + } + ruleMap := map[string]bool{} + for _, r := range firewall.Rule { + ruleStr := fmt.Sprintf("%s|%s|%s|%s|%s", r.ProtocolType, r.DstPort, r.SrcIP, r.RuleAction, r.Priority) + ruleMap[ruleStr] = true + } + if rulesFilePath != "" { + rules, err := parseRulesFromFile(rulesFilePath) + if err != nil { + base.HandleError(err) + return + } + req.Rule = append(req.Rule, rules...) + } + for _, r := range req.Rule { + ruleMap[r] = true + } + req.Rule = []string{} + for r := range ruleMap { + r = strings.TrimSpace(r) + req.Rule = append(req.Rule, r) + } + _, err = base.BizClient.UpdateFirewall(req) + if err != nil { + base.HandleError(err) + return + } + base.Cxt.Printf("firewall[%s] updated\n", fwID) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&fwIDs, "fw-id", nil, "Required. Resource ID of firewalls to update") + flags.StringSliceVar(&req.Rule, "rules", nil, "Required if rules-file is empay. Rules to add to firewall. Schema:'Protocol|Port|IP|Action|Level'. See 'ucloud firewall create --help' for detail.") + flags.StringVar(&rulesFilePath, "rules-file", "", "Required if rules is empty. Path of rules file, in which each rule occupies one line. Schema: Protocol|Port|IP|Action|Level.") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Region, see 'ucloud region'") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Project-id, see 'ucloud project list'") + + flags.SetFlagValuesFunc("fw-id", func() []string { + return getFirewallIDNames(*req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("rules-file", func() []string { + return base.GetFileList("") + }) + + cmd.MarkFlagRequired("fw-id") + return cmd +} + +// NewCmdFirewallDeleteRule ucloud firewall remove-rule +func NewCmdFirewallDeleteRule(out io.Writer) *cobra.Command { + var rulesFilePath string + var fwIDs []string + req := base.BizClient.NewUpdateFirewallRequest() + cmd := &cobra.Command{ + Use: "remove-rule", + Short: "Remove rule from firewall instance", + Long: "Remove rule from firewall instance", + Example: `ucloud firewall remove-rule --fw-id firewall-2cxxxz/test.lxj2 --rules "TCP|24|0.0.0.0/0|ACCEPT|HIGH" --rules-file firewall_rules.txt`, + Run: func(c *cobra.Command, args []string) { + if req.Rule == nil && rulesFilePath == "" { + fmt.Fprintln(out, "Error: flags rules and rules-file can't be both empty") + return + } + for _, fwID := range fwIDs { + id := base.PickResourceID(fwID) + req.FWId = &id + firewall, err := getFirewall(*req.FWId, *req.ProjectId, *req.Region) + if err != nil { + base.HandleError(err) + return + } + ruleMap := map[string]bool{} + for _, r := range firewall.Rule { + ruleStr := fmt.Sprintf("%s|%s|%s|%s|%s", r.ProtocolType, r.DstPort, r.SrcIP, r.RuleAction, r.Priority) + ruleMap[ruleStr] = true + } + if rulesFilePath != "" { + rules, err := parseRulesFromFile(rulesFilePath) + if err != nil { + base.HandleError(err) + return + } + req.Rule = append(req.Rule, rules...) + } + for _, r := range req.Rule { + r = strings.TrimSpace(r) + delete(ruleMap, r) + } + req.Rule = []string{} + for r := range ruleMap { + req.Rule = append(req.Rule, r) + } + if len(req.Rule) == 0 { + fmt.Fprintf(out, "Error: rules can't be all deleted\n") + return + } + _, err = base.BizClient.UpdateFirewall(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintf(out, "firewall[%s] updated\n", fwID) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&fwIDs, "fw-id", nil, "Required. Resource ID of firewalls to update") + flags.StringSliceVar(&req.Rule, "rules", nil, "Required if rules-file is empay. Rules to add to firewall. Schema:'Protocol|Port|IP|Action|Level'. See 'ucloud firewall create --help' for detail.") + flags.StringVar(&rulesFilePath, "rules-file", "", "Required if rules is empty. Path of rules file, in which each rule occupies one line. Schema: Protocol|Port|IP|Action|Level.") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Region, see 'ucloud region'") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Project-id, see 'ucloud project list'") + + flags.SetFlagValuesFunc("fw-id", func() []string { + return getFirewallIDNames(*req.ProjectId, *req.Region) + }) + + cmd.MarkFlagRequired("fw-id") + return cmd +} + +// NewCmdFirewallApply ucloud firewall apply +func NewCmdFirewallApply() *cobra.Command { + req := base.BizClient.NewGrantFirewallRequest() + resourceIDs := []string{} + fwID := "" + cmd := &cobra.Command{ + Use: "apply", + Short: "Applay firewall to ucloud service", + Long: "Applay firewall to ucloud service", + Example: "ucloud firewall apply --fw-id firewall-xxx --resource-id uhost-xxx --resource-type uhost", + Run: func(c *cobra.Command, args []string) { + req.FWId = sdk.String(base.PickResourceID(fwID)) + for _, id := range resourceIDs { + req.ResourceId = sdk.String(id) + _, err := base.BizClient.GrantFirewall(req) + if err != nil { + base.HandleError(err) + continue + } + base.Cxt.Printf("firewall[%s] applied to %s[%s]\n", fwID, *req.ResourceType, id) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringVar(&fwID, "fw-id", "", "Required. Resource ID of firewall to apply to some ucloud resource") + req.ResourceType = flags.String("resource-type", "", "Required. Resource type of resource to be applied firewall. Range 'uhost','unatgw','upm','hadoophost','fortresshost','udhost','udockhost','dbaudit'.") + flags.StringSliceVar(&resourceIDs, "resource-id", nil, "Resource ID of resources to be applied firewall") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Region, see 'ucloud region'") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Project-id, see 'ucloud project list'") + + flags.SetFlagValues("resource-type", "uhost", "unatgw", "upm", "hadoophost", "fortresshost", "udhost", "udockhost", "dbaudit") + flags.SetFlagValuesFunc("fw-id", func() []string { + return getFirewallIDNames(*req.ProjectId, *req.Region) + }) + + cmd.MarkFlagRequired("fw-id") + cmd.MarkFlagRequired("resource-id") + cmd.MarkFlagRequired("resource-type") + + return cmd +} + +// NewCmdFirewallCopy ucloud firewall copy +func NewCmdFirewallCopy() *cobra.Command { + srcFirewall := "" + srcRegion := "" + req := base.BizClient.NewCreateFirewallRequest() + cmd := &cobra.Command{ + Use: "copy", + Short: "Copy firewall", + Long: "Copy firewall", + Example: "ucloud firewall copy --src-fw firewall-xxx --target-region cn-bj2 --name test", + Run: func(c *cobra.Command, args []string) { + fwID := base.PickResourceID(srcFirewall) + firewall, err := getFirewall(fwID, *req.ProjectId, srcRegion) + + if err != nil { + base.HandleError(err) + return + } + req.Tag = sdk.String(firewall.Tag) + req.Remark = sdk.String(firewall.Remark) + for _, r := range firewall.Rule { + rstr := fmt.Sprintf("%s|%s|%s|%s|%s", r.ProtocolType, r.DstPort, r.SrcIP, r.RuleAction, r.Priority) + req.Rule = append(req.Rule, rstr) + } + resp, err := base.BizClient.CreateFirewall(req) + if err != nil { + base.HandleError(err) + return + } + base.Cxt.Printf("firewall[%s] created from %s\n", resp.FWId, srcFirewall) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + flags.StringVar(&srcFirewall, "src-fw", "", "Required. ResourceID or name of source firewall") + req.Name = flags.String("name", "", "Required. Name of new firewall") + flags.StringVar(&srcRegion, "region", base.ConfigIns.Region, "Optional. Current region, used to fetch source firewall") + req.Region = flags.String("target-region", base.ConfigIns.Region, "Optional. Copy firewall to target region") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Project-id, see 'ucloud project list'") + + flags.SetFlagValuesFunc("src-fw", func() []string { + return getFirewallIDNames(*req.ProjectId, srcRegion) + }) + flags.SetFlagValuesFunc("target-region", getRegionList) + flags.SetFlagValuesFunc("region", getRegionList) + + cmd.MarkFlagRequired("src-fw-id") + cmd.MarkFlagRequired("name") + + return cmd +} + +// NewCmdFirewallDelete ucloud firewall delete +func NewCmdFirewallDelete() *cobra.Command { + req := base.BizClient.NewDeleteFirewallRequest() + ids := []string{} + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete firewall by resource ids or names", + Long: "Delete firewall by resource ids or names", + Example: "ucloud firewall delete --fw-id firewall-xxx", + Run: func(c *cobra.Command, args []string) { + for _, id := range ids { + req.FWId = sdk.String(base.PickResourceID(id)) + _, err := base.BizClient.DeleteFirewall(req) + if err != nil { + base.HandleError(err) + return + } + base.Cxt.Printf("firewall[%s] deleted\n", id) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + flags.StringSliceVar(&ids, "fw-id", nil, "Required. Resource IDs of firewall to delete") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Region, see 'ucloud region'") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Project-id, see 'ucloud project list'") + + cmd.MarkFlagRequired("fw-id") + flags.SetFlagValuesFunc("fw-id", func() []string { + return getFirewallIDNames(*req.ProjectId, *req.Region) + }) + + return cmd +} + +// FirewallResourceRow 表格行 +type FirewallResourceRow struct { + ResourceName string + ResourceID string + ResourceType string + IntranetIP string + Group string + Remark string +} + +// NewCmdFirewallResource ucloud firewall resource +func NewCmdFirewallResource(out io.Writer) *cobra.Command { + fwID := "" + req := base.BizClient.NewDescribeFirewallResourceRequest() + cmd := &cobra.Command{ + Use: "resource", + Short: "List resources that has been applied the firewall", + Long: "List resources that has been applied the firewall", + Run: func(c *cobra.Command, args []string) { + req.FWId = sdk.String(base.PickResourceID(fwID)) + resp, err := base.BizClient.DescribeFirewallResource(req) + if err != nil { + base.HandleError(err) + return + } + list := []FirewallResourceRow{} + for _, rs := range resp.ResourceSet { + row := FirewallResourceRow{} + row.ResourceName = rs.Name + row.ResourceID = rs.ResourceID + row.ResourceType = rs.ResourceType + row.IntranetIP = rs.PrivateIP + row.Group = rs.Tag + row.Remark = rs.Remark + list = append(list, row) + } + base.PrintList(list, out) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringVar(&fwID, "fw-id", "", "Required. Resource ID of firewall") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Region, see 'ucloud region'") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Project-id, see 'ucloud project list'") + req.Offset = flags.Int("offset", 0, "Optional. Offset") + req.Limit = flags.Int("limit", 50, "Optional. Limit") + + flags.SetFlagValuesFunc("fw-id", func() []string { + return getFirewallIDNames(*req.ProjectId, *req.Region) + }) + + cmd.MarkFlagRequired("fw-id") + + return cmd +} + +// NewCmdFirewallUpdate ucloud firewall update +func NewCmdFirewallUpdate(out io.Writer) *cobra.Command { + fwIDs := []string{} + req := base.BizClient.NewUpdateFirewallAttributeRequest() + cmd := &cobra.Command{ + Use: "update", + Short: "Update firewall attribute, such as name,group and remark.", + Long: "Update firewall attribute, such as name,group and remark.", + Example: `ucloud firewall update --fw-id firewall-2xxxx/test2 --name test_update.1 --remark "this is a remark"`, + Run: func(c *cobra.Command, args []string) { + if *req.Name == "" && *req.Tag == "" && *req.Remark == "" { + fmt.Fprintln(out, "Error: name, group and remark can't be all empty") + return + } + if *req.Name == "" { + req.Name = nil + } + if *req.Tag == "" { + req.Tag = nil + } + if *req.Remark == "" { + req.Remark = nil + } + for _, id := range fwIDs { + req.FWId = sdk.String(base.PickResourceID(id)) + _, err := base.BizClient.UpdateFirewallAttribute(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintf(out, "firewall[%s] updated\n", id) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&fwIDs, "fw-id", nil, "Required. Resource ID of firewalls") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Region, see 'ucloud region'") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Project-id, see 'ucloud project list'") + req.Name = flags.String("name", "", "Name of firewall") + req.Tag = flags.String("group", "", "Group of firewall") + req.Remark = flags.String("remark", "", "Remark of firewall") + + flags.SetFlagValuesFunc("fw-id", func() []string { + return getFirewallIDNames(*req.ProjectId, *req.Region) + }) + + cmd.MarkFlagRequired("fw-id") + + return cmd +} + +func getFirewallIDNames(project, region string) (idNames []string) { + list, err := getAllFirewallIns(project, region) + if err != nil { + return + } + for _, f := range list { + idNames = append(idNames, f.FWId+"/"+f.Name) + } + return +} + +func getFirewall(fwNameID, project, region string) (*unet.FirewallDataSet, error) { + var firewall *unet.FirewallDataSet + list, err := getAllFirewallIns(project, region) + if err != nil { + return nil, err + } + for i, fw := range list { + if fw.FWId == fwNameID || fw.Name == fwNameID { + firewall = &list[i] + } + } + if firewall == nil { + return nil, fmt.Errorf("firwall[%s] does not exist", fwNameID) + } + return firewall, nil +} + +func getAllFirewallIns(project, region string) ([]unet.FirewallDataSet, error) { + req := base.BizClient.NewDescribeFirewallRequest() + req.ProjectId = sdk.String(project) + req.Region = sdk.String(region) + list := []unet.FirewallDataSet{} + for offset, limit := 0, 100; ; offset += limit { + req.Offset = sdk.Int(offset) + req.Limit = sdk.Int(limit) + resp, err := base.BizClient.DescribeFirewall(req) + if err != nil { + return nil, err + } + for _, fw := range resp.DataSet { + list = append(list, fw) + } + if resp.TotalCount < offset+limit { + break + } + } + return list, nil +} diff --git a/cmd/globalssh.go b/cmd/globalssh.go index 999e2d5725..6fcfbdb6f8 100644 --- a/cmd/globalssh.go +++ b/cmd/globalssh.go @@ -15,23 +15,28 @@ package cmd import ( + "fmt" + "io" "net" "strings" "github.com/spf13/cobra" - . "github.com/ucloud/ucloud-cli/base" + "github.com/ucloud/ucloud-sdk-go/services/pathx" sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + + "github.com/ucloud/ucloud-cli/base" ) -//NewCmdGssh ucloud gssh +// NewCmdGssh ucloud gssh func NewCmdGssh() *cobra.Command { cmd := &cobra.Command{ Use: "gssh", Short: "Create,list,update and delete globalssh instance", Long: `Create,list,update and delete globalssh instance`, } - cmd.AddCommand(NewCmdGsshList()) + out := base.Cxt.GetWriter() + cmd.AddCommand(NewCmdGsshList(out)) cmd.AddCommand(NewCmdGsshCreate()) cmd.AddCommand(NewCmdGsshDelete()) cmd.AddCommand(NewCmdGsshModify()) @@ -39,19 +44,21 @@ func NewCmdGssh() *cobra.Command { return cmd } -//GSSHRow gssh表格行 +// GSSHRow gssh表格行 type GSSHRow struct { ResourceID string SSHServerIP string AcceleratingDomain string SSHServerLocation string SSHPort int + GlobalSSHPort int Remark string + InstanceType string } -//NewCmdGsshList ucloud gssh list -func NewCmdGsshList() *cobra.Command { - req := BizClient.NewDescribeGlobalSSHInstanceRequest() +// NewCmdGsshList ucloud gssh list +func NewCmdGsshList(out io.Writer) *cobra.Command { + req := base.BizClient.NewDescribeGlobalSSHInstanceRequest() cmd := &cobra.Command{ Use: "list", Short: "List all GlobalSSH instances", @@ -65,52 +72,51 @@ func NewCmdGsshList() *cobra.Command { "东京": "Tokyo", "华盛顿": "Washington", "法兰克福": "Frankfurt", + "拉各斯": "Lagos", } - resp, err := BizClient.DescribeGlobalSSHInstance(req) + resp, err := base.BizClient.DescribeGlobalSSHInstance(req) if err != nil { - HandleError(err) + base.HandleError(err) } else { - if global.json { - PrintJSON(resp.InstanceSet) - } else { - list := make([]GSSHRow, 0) - for _, gssh := range resp.InstanceSet { - row := GSSHRow{} - row.ResourceID = gssh.InstanceId - row.SSHServerIP = gssh.TargetIP - row.AcceleratingDomain = gssh.AcceleratingDomain - row.SSHPort = gssh.Port - row.Remark = gssh.Remark - if val, ok := areaMap[gssh.Area]; ok { - row.SSHServerLocation = val - } else { - row.SSHServerLocation = gssh.Area - } - list = append(list, row) + list := make([]GSSHRow, 0) + for _, gssh := range resp.InstanceSet { + row := GSSHRow{} + row.ResourceID = gssh.InstanceId + row.SSHServerIP = gssh.TargetIP + row.AcceleratingDomain = gssh.AcceleratingDomain + row.SSHPort = gssh.Port + row.GlobalSSHPort = gssh.GlobalSSHPort + row.Remark = gssh.Remark + row.InstanceType = gssh.InstanceType + if val, ok := areaMap[gssh.Area]; ok { + row.SSHServerLocation = val + } else { + row.SSHServerLocation = gssh.Area } - PrintTable(list, []string{"ResourceID", "SSHServerIP", "AcceleratingDomain", "SSHServerLocation", "SSHPort", "Remark"}) + list = append(list, row) } + base.PrintList(list, out) } }, } cmd.Flags().SortFlags = false - req.Region = cmd.Flags().String("region", ConfigInstance.Region, "Optional. Assign region") - req.ProjectId = cmd.Flags().String("project-id", ConfigInstance.ProjectID, "Optional. Assign project-id") + req.Region = cmd.Flags().String("region", base.ConfigIns.Region, "Optional. Assign region") + req.ProjectId = cmd.Flags().String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") return cmd } -//NewCmdGsshArea ucloud gssh area +// NewCmdGsshArea ucloud gssh area func NewCmdGsshArea() *cobra.Command { - req := BizClient.NewDescribeGlobalSSHAreaRequest() + req := base.BizClient.NewDescribeGlobalSSHAreaRequest() cmd := &cobra.Command{ Use: "location", Short: "List SSH server locations and covered areas", Long: "List SSH server locations and covered areas", Run: func(cmd *cobra.Command, args []string) { - resp, err := BizClient.DescribeGlobalSSHArea(req) + resp, err := base.BizClient.DescribeGlobalSSHArea(req) if err != nil { - HandleError(err) + base.HandleError(err) return } list := make([]GsshLocation, 0) @@ -121,19 +127,19 @@ func NewCmdGsshArea() *cobra.Command { } regionLabels := make([]string, 0) for _, region := range item.RegionSet { - regionLabels = append(regionLabels, RegionLabel[region]) + regionLabels = append(regionLabels, base.RegionLabel[region]) } row.CoveredArea = strings.Join(regionLabels, ",") list = append(list, row) } - PrintTable(list, []string{"AirportCode", "SSHServerLocation", "CoveredArea"}) + base.PrintTable(list, []string{"AirportCode", "SSHServerLocation", "CoveredArea"}) }, } return cmd } -//GsshLocation 服务地点和覆盖区域 +// GsshLocation 服务地点和覆盖区域 type GsshLocation struct { AirportCode string SSHServerLocation string @@ -147,12 +153,13 @@ var areaCodeMap = map[string]string{ "HND": "Tokyo", "IAD": "Washington", "FRA": "Frankfurt", + "LOS": "Lagos", } -//NewCmdGsshCreate ucloud gssh create +// NewCmdGsshCreate ucloud gssh create func NewCmdGsshCreate() *cobra.Command { var targetIP *net.IP - req := BizClient.NewCreateGlobalSSHInstanceRequest() + req := base.BizClient.NewCreateGlobalSSHInstanceRequest() cmd := &cobra.Command{ Use: "create", Short: "Create GlobalSSH instance", @@ -165,114 +172,160 @@ func NewCmdGsshCreate() *cobra.Command { *req.AreaCode = code } } - if port < 1 || port > 65535 || port == 80 || port == 443 { - Cxt.Println("The port number should be between 1 and 65535, and cannot be 80 or 443") + if port < 1 || port > 65535 || port == 80 || port == 443 || port == 65123 { + base.Cxt.Println("The port number should be between 1 and 65535, and cannot be 80, 443 or 65123") return } req.TargetIP = sdk.String(targetIP.String()) - resp, err := BizClient.CreateGlobalSSHInstance(req) + resp, err := base.BizClient.CreateGlobalSSHInstance(req) if err != nil { - HandleError(err) + base.HandleError(err) } else { - Cxt.Printf("gssh[%s] created\n", resp.InstanceId) + base.Cxt.Printf("gssh[%s] created\n", resp.InstanceId) } }, } - cmd.Flags().SortFlags = false + flags := cmd.Flags() + flags.SortFlags = false + req.AreaCode = cmd.Flags().String("location", "", "Required. Location of the source server. See 'ucloud gssh location'") targetIP = cmd.Flags().IP("target-ip", nil, "Required. IP of the source server. Required") - req.Region = cmd.Flags().String("region", "", "Optional. Assign region") - req.ProjectId = cmd.Flags().String("project-id", ConfigInstance.ProjectID, "Optional. Assign project-id") - req.Port = cmd.Flags().Int("port", 22, "Optional. Port of The SSH service between 1 and 65535. Do not use ports such as 80,443.") + bindProjectID(req, flags) + req.Port = cmd.Flags().Int("port", 22, "Optional. Port of The SSH service between 1 and 65535. Do not use ports such as 80, 443 or 65123.") req.Remark = cmd.Flags().String("remark", "", "Optional. Remark of your GlobalSSH.") req.ChargeType = cmd.Flags().String("charge-type", "Month", "Optional.'Year',pay yearly;'Month',pay monthly;'Dynamic', pay hourly(requires access)") req.Quantity = cmd.Flags().Int("quantity", 1, "Optional. The duration of the instance. N years/months.") - req.CouponId = cmd.Flags().String("coupon-id", "", "Optional. Coupon ID, The Coupon can deduct part of the payment,see DescribeCoupon or https://accountv2.ucloud.cn") + req.InstanceType = cmd.Flags().String("instance-type", "", "Optional. Possible values: 'Ultimate','Enterprise', 'Basic', 'Free'(Default value)") + req.ForwardRegion = cmd.Flags().String("forward-region", "", "Optional. You can select one of 'cn-bj2','cn-sh2','cn-gd' When instance-type is 'Basic'") + req.BandwidthPackage = cmd.Flags().Int("bandwidth-package", 0, "Optional. You can set one of 0, 20, 40 When instance-type is 'Ultimate'") cmd.MarkFlagRequired("location") cmd.MarkFlagRequired("target-ip") - cmd.Flags().SetFlagValues("location", "LosAngeles", "Singapore", "HongKong", "Tokyo", "Washington", "Frankfurt") + cmd.Flags().SetFlagValues("location", "LosAngeles", "Singapore", "Lagos", "HongKong", "Tokyo", "Washington", "Frankfurt") cmd.Flags().SetFlagValues("charge-type", "Month", "Year", "Dynamic", "Trial") + cmd.Flags().SetFlagValues("bandwidth-package", "0", "20", "40") + cmd.Flags().SetFlagValues("forward-region", "cn-bj2", "cn-sh2", "cn-gd") + cmd.Flags().SetFlagValues("instance-type", "Free", "Basic", "Enterprise", "Ultimate") + cmd.Flags().SetFlagValuesFunc("target-ip", func() []string { + eips := getAllEip(*req.ProjectId, base.ConfigIns.Region, nil, nil) + for idx, eip := range eips { + eips[idx] = strings.SplitN(eip, "/", 2)[1] + } + return eips + }) return cmd } -//NewCmdGsshDelete ucloud gssh delete +// NewCmdGsshDelete ucloud gssh delete func NewCmdGsshDelete() *cobra.Command { - var req = BizClient.NewDeleteGlobalSSHInstanceRequest() + var req = base.BizClient.NewDeleteGlobalSSHInstanceRequest() var gsshIds *[]string var cmd = &cobra.Command{ Use: "delete", Short: "Delete GlobalSSH instance", Long: "Delete GlobalSSH instance", - Example: "ucloud gssh delete --resource-id uga-xx1 --id uga-xx2", + Example: "ucloud gssh delete --gssh-id uga-xx1 --id uga-xx2", Run: func(cmd *cobra.Command, args []string) { + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) for _, id := range *gsshIds { - req.InstanceId = &id - _, err := BizClient.DeleteGlobalSSHInstance(req) + req.InstanceId = sdk.String(base.PickResourceID(id)) + _, err := base.BizClient.DeleteGlobalSSHInstance(req) if err != nil { - HandleError(err) + base.HandleError(err) } else { - Cxt.Printf("gssh[%s] deleted\n", id) + base.Cxt.Printf("gssh[%s] deleted\n", id) } } }, } - cmd.Flags().SortFlags = false - gsshIds = cmd.Flags().StringArray("resource-id", make([]string, 0), "Required. ID of the GlobalSSH instances you want to delete. Multiple values specified by multiple flags") - req.ProjectId = cmd.Flags().String("project-id", ConfigInstance.ProjectID, "Optional. Assign project-id") - req.Region = cmd.Flags().String("region", ConfigInstance.Region, "Optional. Assign region") - cmd.MarkFlagRequired("resource-id") + flags := cmd.Flags() + flags.SortFlags = false + gsshIds = cmd.Flags().StringSlice("gssh-id", make([]string, 0), "Required. ID of the GlobalSSH instances you want to delete. Multiple values specified by multiple commas") + bindProjectID(req, flags) + cmd.MarkFlagRequired("gssh-id") + cmd.Flags().SetFlagValuesFunc("gssh-id", func() []string { + return getAllGsshIDNames(*req.ProjectId) + }) return cmd } -//NewCmdGsshModify ucloud gssh modify +// NewCmdGsshModify ucloud gssh modify func NewCmdGsshModify() *cobra.Command { - var gsshModifyPortReq = BizClient.NewModifyGlobalSSHPortRequest() - var gsshModifyRemarkReq = BizClient.NewModifyGlobalSSHRemarkRequest() - region := ConfigInstance.Region - project := ConfigInstance.ProjectID - var cmd = &cobra.Command{ + gsshModifyPortReq := base.BizClient.NewModifyGlobalSSHPortRequest() + gsshModifyRemarkReq := base.BizClient.NewModifyGlobalSSHRemarkRequest() + project := base.ConfigIns.ProjectID + gsshIDs := []string{} + cmd := &cobra.Command{ Use: "update", Short: "Update GlobalSSH instance", Long: "Update GlobalSSH instance, including port and remark attribute", - Example: "ucloud gssh update --resource-id uga-xxx --port 22", + Example: "ucloud gssh update --gssh-id uga-xxx --port 22", Run: func(cmd *cobra.Command, args []string) { - gsshModifyPortReq.Region = sdk.String(region) gsshModifyPortReq.ProjectId = sdk.String(project) - gsshModifyRemarkReq.Region = sdk.String(region) gsshModifyRemarkReq.ProjectId = sdk.String(project) if *gsshModifyPortReq.Port == 0 && *gsshModifyRemarkReq.Remark == "" { - Cxt.Println("port or remark required") + base.Cxt.Println("Error, port or remark required") } if *gsshModifyPortReq.Port != 0 { port := *gsshModifyPortReq.Port - if port <= 1 || port >= 65535 || port == 80 || port == 443 { - Cxt.Println("The port number should be between 1 and 65535, and cannot be equal to 80 or 443") + if port <= 1 || port >= 65535 || port == 80 || port == 443 || port == 65123 { + base.Cxt.Println("The port number should be between 1 and 65535, and cannot be equal to 80, 443 or 65123") return } - gsshModifyPortReq.InstanceId = gsshModifyRemarkReq.InstanceId - _, err := BizClient.ModifyGlobalSSHPort(gsshModifyPortReq) - if err != nil { - HandleError(err) - } else { - Cxt.Printf("gssh[%s] updated\n", *gsshModifyPortReq.InstanceId) + for _, idname := range gsshIDs { + gsshModifyPortReq.InstanceId = sdk.String(base.PickResourceID(idname)) + _, err := base.BizClient.ModifyGlobalSSHPort(gsshModifyPortReq) + if err != nil { + base.HandleError(err) + } else { + base.Cxt.Printf("gssh[%s]'s port updated\n", *gsshModifyPortReq.InstanceId) + } } } if *gsshModifyRemarkReq.Remark != "" { - _, err := BizClient.ModifyGlobalSSHRemark(gsshModifyRemarkReq) - if err != nil { - HandleError(err) - } else { - Cxt.Printf("gssh[%s] updated\n", *gsshModifyRemarkReq.InstanceId) + for _, idname := range gsshIDs { + gsshModifyRemarkReq.InstanceId = sdk.String(base.PickResourceID(idname)) + _, err := base.BizClient.ModifyGlobalSSHRemark(gsshModifyRemarkReq) + if err != nil { + base.HandleError(err) + } else { + base.Cxt.Printf("gssh[%s]'s remark updated\n", *gsshModifyRemarkReq.InstanceId) + } } } }, } - cmd.Flags().SortFlags = false - gsshModifyRemarkReq.InstanceId = cmd.Flags().String("resource-id", "", "Required. InstanceID of your GlobalSSH") - cmd.Flags().StringVar(®ion, "region", ConfigInstance.Region, "Optional. Assign region") - cmd.Flags().StringVar(&project, "project-id", ConfigInstance.ProjectID, "Optional. Assign project-id") + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&gsshIDs, "gssh-id", nil, "Required. ResourceID of your GlobalSSH instances") + bindProjectIDS(&project, flags) gsshModifyPortReq.Port = cmd.Flags().Int("port", 0, "Optional. Port of SSH service.") gsshModifyRemarkReq.Remark = cmd.Flags().String("remark", "", "Optional. Remark of your GlobalSSH.") - cmd.MarkFlagRequired("resource-id") + cmd.MarkFlagRequired("gssh-id") + cmd.Flags().SetFlagValuesFunc("gssh-id", func() []string { + return getAllGsshIDNames(project) + }) return cmd } + +func getAllGssh(project string) ([]pathx.GlobalSSHInfo, error) { + req := base.BizClient.NewDescribeGlobalSSHInstanceRequest() + req.ProjectId = &project + resp, err := base.BizClient.DescribeGlobalSSHInstance(req) + if err != nil { + return nil, err + } + return resp.InstanceSet, nil +} + +func getAllGsshIDNames(project string) []string { + gsshs, err := getAllGssh(project) + if err != nil { + return nil + } + list := []string{} + for _, gssh := range gsshs { + list = append(list, fmt.Sprintf("%s/%s", gssh.InstanceId, gssh.TargetIP)) + } + return list +} diff --git a/cmd/image.go b/cmd/image.go index ecce651e6c..c3ed24925b 100644 --- a/cmd/image.go +++ b/cmd/image.go @@ -15,78 +15,225 @@ package cmd import ( + "fmt" + "io" "strings" "github.com/spf13/cobra" - . "github.com/ucloud/ucloud-cli/base" + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + + "github.com/ucloud/ucloud-cli/base" + "github.com/ucloud/ucloud-cli/model/cli" + "github.com/ucloud/ucloud-cli/model/status" ) -//NewCmdUImage ucloud uimage +// NewCmdUImage ucloud uimage func NewCmdUImage() *cobra.Command { cmd := &cobra.Command{ Use: "image", - Short: "List images", - Long: `List images`, + Short: "List and manipulate images", + Long: `List and manipulate images`, Args: cobra.NoArgs, } - cmd.AddCommand(NewCmdUImageList()) + writer := base.Cxt.GetWriter() + cmd.AddCommand(NewCmdUImageList(writer)) + cmd.AddCommand(NewCmdImageCopy(writer)) + cmd.AddCommand(NewCmdUImageDelete()) + createImageCmd := NewCmdUhostCreateImage(writer) + createImageCmd.Use = "create" + cmd.AddCommand(createImageCmd) return cmd } -//ImageRow 表格行 +// ImageRow 表格行 type ImageRow struct { ImageName string ImageID string + ImageType string BasicImage string ExtensibleFeature string CreationTime string State string } -//NewCmdUImageList ucloud uimage list -func NewCmdUImageList() *cobra.Command { - req := BizClient.NewDescribeImageRequest() +// NewCmdUImageList ucloud uimage list +func NewCmdUImageList(out io.Writer) *cobra.Command { + req := base.BizClient.NewDescribeImageRequest() cmd := &cobra.Command{ Use: "list", Short: "List image", Long: "List image", Example: "ucloud image list --image-type Base", Run: func(cmd *cobra.Command, args []string) { - resp, err := BizClient.DescribeImage(req) + resp, err := base.BizClient.DescribeImage(req) if err != nil { - HandleError(err) + base.HandleError(err) return } - if global.json { - PrintJSON(resp.ImageSet) - } else { - list := make([]ImageRow, 0) - for _, image := range resp.ImageSet { - row := ImageRow{} - row.ImageName = image.ImageName - row.ImageID = image.ImageId - row.BasicImage = image.OsName - row.ExtensibleFeature = strings.Join(image.Features, ",") - row.CreationTime = FormatDate(image.CreateTime) - row.State = image.State - if row.State == "Available" { - list = append(list, row) - } + list := make([]ImageRow, 0) + for _, image := range resp.ImageSet { + row := ImageRow{} + row.ImageName = image.ImageName + row.ImageID = image.ImageId + row.ImageType = image.ImageType + row.BasicImage = image.OsName + row.ExtensibleFeature = strings.Join(image.Features, ",") + row.CreationTime = base.FormatDate(image.CreateTime) + row.State = image.State + if row.State == "Available" { + list = append(list, row) } - PrintTable(list, []string{"ImageName", "ImageID", "BasicImage", "ExtensibleFeature", "CreationTime"}) } + base.PrintList(list, out) }, } - req.ProjectId = cmd.Flags().String("project-id", ConfigInstance.ProjectID, "Assign project-id") - req.Region = cmd.Flags().String("region", ConfigInstance.Region, "Assign region") - req.Zone = cmd.Flags().String("zone", ConfigInstance.Zone, "Assign availability zone") - req.ImageType = cmd.Flags().String("image-type", "", "'Base',Standard image; 'Business',image market; 'Custom',custom image; Return all types by default") - req.OsType = cmd.Flags().String("os-type", "", "Linux or Windows. Return all types by default") - req.ImageId = cmd.Flags().String("image-id", "", "iamge id such as 'uimage-xxx'") - req.Offset = cmd.Flags().Int("offset", 0, "offset default 0") - req.Limit = cmd.Flags().Int("limit", 500, "max count") + req.ProjectId = cmd.Flags().String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + req.Region = cmd.Flags().String("region", base.ConfigIns.Region, "Optional. Assign region") + req.Zone = cmd.Flags().String("zone", "", "Optional. Assign availability zone") + req.ImageType = cmd.Flags().String("image-type", "Base", "Optional. 'Base',Standard image; 'Business',image market; 'Custom',custom image") + req.OsType = cmd.Flags().String("os-type", "", "Optional. Linux or Windows. Return all types by default") + req.ImageId = cmd.Flags().String("image-id", "", "Optional. Resource ID of image") + req.Offset = cmd.Flags().Int("offset", 0, "Optional. Offset default 0") + req.Limit = cmd.Flags().Int("limit", 500, "Optional. Max count") cmd.Flags().SetFlagValues("image-type", "Base", "Business", "Custom") return cmd } + +// func NewCmdImageImport() *cobra.Command { +// req := BizClient.NewImportCustomImageRequest() +// } + +// NewCmdUImageDelete ucloud image delete +func NewCmdUImageDelete() *cobra.Command { + var imageIDs *[]string + req := base.BizClient.NewTerminateCustomImageRequest() + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete custom images", + Long: "Delete custom images", + Run: func(cmd *cobra.Command, args []string) { + for _, id := range *imageIDs { + req.ImageId = sdk.String(base.PickResourceID(id)) + resp, err := base.BizClient.TerminateCustomImage(req) + if err != nil { + base.HandleError(err) + return + } + base.Cxt.Printf("image[%s] deleted\n", resp.ImageId) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + imageIDs = cmd.Flags().StringSlice("image-id", nil, "Required. Resource ID of images") + req.ProjectId = cmd.Flags().String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + req.Region = cmd.Flags().String("region", base.ConfigIns.Region, "Optional. Assign region") + req.Zone = cmd.Flags().String("zone", "", "Optional. Assign availability zone") + cmd.MarkFlagRequired("image-id") + flags.SetFlagValuesFunc("image-id", func() []string { + return getImageList([]string{status.IMAGE_AVAILABLE, status.IMAGE_COPYING, status.IMAGE_MAKING}, cli.IAMGE_CUSTOM, *req.ProjectId, *req.Region, "") + }) + return cmd +} + +// NewCmdImageCopy ucloud image copy +func NewCmdImageCopy(out io.Writer) *cobra.Command { + var imageIDs *[]string + var async *bool + req := base.BizClient.NewCopyCustomImageRequest() + cmd := &cobra.Command{ + Use: "copy", + Short: "Copy custom images", + Long: "Copy custom images", + Run: func(c *cobra.Command, args []string) { + *req.ProjectId = base.PickResourceID(*req.ProjectId) + *req.TargetProjectId = base.PickResourceID(*req.TargetProjectId) + for _, id := range *imageIDs { + id = base.PickResourceID(id) + req.SourceImageId = &id + resp, err := base.BizClient.CopyCustomImage(req) + if err != nil { + base.HandleError(err) + return + } + text := fmt.Sprintf("image[%s] is coping", resp.TargetImageId) + if *async { + fmt.Fprintln(out, text) + } else { + poller := base.NewPoller(describeImageByID, out) + poller.Poll(resp.TargetImageId, *req.TargetProjectId, *req.TargetRegion, "", text, []string{status.IMAGE_AVAILABLE, status.IMAGE_UNAVAILABLE}) + } + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + imageIDs = cmd.Flags().StringSlice("source-image-id", nil, "Required. Resource ID of source image") + req.ProjectId = cmd.Flags().String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + req.Region = cmd.Flags().String("region", base.ConfigIns.Region, "Optional. Assign region") + req.Zone = cmd.Flags().String("zone", base.ConfigIns.Zone, "Optional. Assign availability zone") + req.TargetRegion = flags.String("target-region", base.ConfigIns.Region, "Optional. Target region. See 'ucloud region'") + req.TargetProjectId = flags.String("target-project", base.ConfigIns.ProjectID, "Optional. Target Project ID. See 'ucloud project list'") + req.TargetImageName = flags.String("target-image-name", "", "Optional. Name of target image") + req.TargetImageDescription = flags.String("target-image-desc", "", "Optional. Description of target image") + async = flags.Bool("async", false, "Optional. Do not wait for the long-running operation to finish.") + + flags.SetFlagValuesFunc("source-image-id", func() []string { + return getImageList([]string{status.IMAGE_AVAILABLE}, cli.IAMGE_CUSTOM, *req.ProjectId, *req.Region, *req.Zone) + }) + flags.SetFlagValuesFunc("project-id", getProjectList) + flags.SetFlagValuesFunc("region", getRegionList) + flags.SetFlagValuesFunc("zone", func() []string { + return getZoneList(*req.Region) + }) + flags.SetFlagValuesFunc("target-region", getRegionList) + flags.SetFlagValuesFunc("target-project", getProjectList) + + cmd.MarkFlagRequired("source-image-id") + + return cmd +} + +func getImageList(states []string, imageType, project, region, zone string) []string { + req := base.BizClient.NewDescribeImageRequest() + req.ProjectId = &project + req.Region = ®ion + req.Zone = &zone + req.Limit = sdk.Int(1000) + if imageType != cli.IMAGE_ALL { + req.ImageType = sdk.String(imageType) + } + resp, err := base.BizClient.DescribeImage(req) + if err != nil { + return nil + } + list := []string{} + for _, image := range resp.ImageSet { + for _, s := range states { + if image.State == s { + list = append(list, image.ImageId+"/"+image.ImageName) + } + } + } + return list +} + +func describeImageByID(imageID, project, region, zone string) (interface{}, error) { + req := base.BizClient.NewDescribeImageRequest() + req.ImageId = sdk.String(imageID) + req.ProjectId = sdk.String(project) + req.Region = sdk.String(region) + req.Zone = sdk.String(zone) + req.Limit = sdk.Int(50) + resp, err := base.BizClient.DescribeImage(req) + if err != nil { + return nil, err + } + if len(resp.ImageSet) < 1 { + return nil, nil + } + return &resp.ImageSet[0], nil +} diff --git a/cmd/mysql.go b/cmd/mysql.go new file mode 100644 index 0000000000..e6de66a868 --- /dev/null +++ b/cmd/mysql.go @@ -0,0 +1,900 @@ +// Copyright © 2018 NAME HERE tony.li@ucloud.cn +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "io" + "strconv" + "strings" + "time" + + "github.com/spf13/cobra" + "github.com/ucloud/ucloud-sdk-go/ucloud/request" + + "github.com/ucloud/ucloud-sdk-go/services/udb" + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + + "github.com/ucloud/ucloud-cli/base" + "github.com/ucloud/ucloud-cli/model/status" +) + +var dbVersionList = []string{"mysql-5.1", "mysql-5.5", "mysql-5.6", "mysql-5.7", "percona-5.5", "percona-5.6", "percona-5.7", "mariadb-10.0"} +var dbDiskTypeList = []string{"normal", "sata_ssd", "pcie_ssd"} + +var poller = base.NewSpoller(describeUdbByID, base.Cxt.GetWriter()) + +// NewCmdMysql ucloud mysql +func NewCmdMysql() *cobra.Command { + cmd := &cobra.Command{ + Use: "mysql", + Short: "Manipulate MySQL on UCloud platform", + Long: "Manipulate MySQL on UCloud platform", + } + out := base.Cxt.GetWriter() + cmd.AddCommand(NewCmdMysqlDB(out)) + cmd.AddCommand(NewCmdUDBConf()) + cmd.AddCommand(NewCmdUDBBackup()) + cmd.AddCommand(NewCmdUDBLog()) + return cmd +} + +// NewCmdMysqlDB ucloud mysql db +func NewCmdMysqlDB(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "db", + Short: "Manange MySQL instances", + Long: "Manange MySQL instances", + } + + cmd.AddCommand(NewCmdUDBList(out)) + cmd.AddCommand(NewCmdMysqlCreate(out)) + cmd.AddCommand(NewCmdUDBDelete(out)) + cmd.AddCommand(NewCmdUDBStart(out)) + cmd.AddCommand(NewCmdUDBStop(out)) + cmd.AddCommand(NewCmdUDBRestart(out)) + cmd.AddCommand(NewCmdUDBResize(out)) + cmd.AddCommand(NewCmdUDBRestore(out)) + cmd.AddCommand(NewCmdUDBResetPassword(out)) + cmd.AddCommand(NewCmdUDBCreateSlave(out)) + cmd.AddCommand(NewCmdUDBPromoteSlave(out)) + // cmd.AddCommand(NewCmdUDBPromoteToHA(out)) + + return cmd +} + +// NewCmdMysqlCreate ucloud mysql create +func NewCmdMysqlCreate(out io.Writer) *cobra.Command { + var confID, diskType string + var backupID int + var async bool + req := base.BizClient.NewCreateUDBInstanceRequest() + cmd := &cobra.Command{ + Use: "create", + Short: "Create MySQL instance on UCloud platform", + Long: "Create MySQL instance on UCloud platform", + Run: func(c *cobra.Command, args []string) { + confID = base.PickResourceID(confID) + id, err := strconv.Atoi(confID) + if err != nil { + base.HandleError(err) + return + } + req.ParamGroupId = &id + if len(*req.Name) < 6 { + fmt.Fprintln(out, "Error, length of name shoud be larger than 5") + return + } + if *req.DiskSpace > 3000 || *req.DiskSpace < 20 { + fmt.Fprintln(out, "Error, disk-size-gb should be between 20 and 3000") + return + } + if *req.MemoryLimit < 1 || *req.MemoryLimit > 128 { + fmt.Fprintln(out, "Error, memory-size-gb should be between 1 and 128") + return + } + if backupID != -1 { + req.BackupId = &backupID + } + *req.MemoryLimit = *req.MemoryLimit * 1000 + switch diskType { + case "normal": + req.UseSSD = sdk.Bool(false) + case "sata_ssd": + req.UseSSD = sdk.Bool(true) + req.SSDType = sdk.String("SATA") + case "pcie_ssd": + req.UseSSD = sdk.Bool(true) + req.SSDType = sdk.String("PCI-E") + default: + if diskType != "" { + req.UseSSD = sdk.Bool(true) + req.SSDType = sdk.String(diskType) + } + } + resp, err := base.BizClient.CreateUDBInstance(req) + if err != nil { + base.HandleError(err) + return + } + text := fmt.Sprintf("udb[%s] is initializing", resp.DBId) + if async { + fmt.Fprintf(out, "udb[%s] is initializing\n", resp.DBId) + } else { + poller.Spoll(resp.DBId, text, []string{status.UDB_RUNNING, status.UDB_FAIL}) + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + bindProjectID(req, flags) + bindRegion(req, flags) + bindZone(req, flags) + req.DBTypeId = flags.String("version", "", "Required. Version of udb instance") + req.Name = flags.String("name", "", "Required. Name of udb instance to create, at least 6 letters") + flags.StringVar(&confID, "conf-id", "", "Required. ConfID of configuration. see 'ucloud mysql conf list'") + req.AdminUser = flags.String("admin-user-name", "root", "Optional. Name of udb instance's administrator") + req.AdminPassword = flags.String("password", "", "Required. Password of udb instance's administrator") + flags.IntVar(&backupID, "backup-id", -1, "Optional. BackupID of the backup which the newly created UDB instance will recover from if specified. See 'ucloud mysql backup list'") + req.Port = flags.Int("port", 3306, "Optional. Port of udb instance") + flags.StringVar(&diskType, "disk-type", "", "Optional. Setting this flag means using SSD disk. Accept values: 'normal','sata_ssd','pcie_ssd'") + req.DiskSpace = flags.Int("disk-size-gb", 20, "Optional. Disk size of udb instance. From 20 to 3000 according to memory size. Unit GB") + req.MemoryLimit = flags.Int("memory-size-gb", 1, "Optional. Memory size of udb instance. From 1 to 128. Unit GB") + req.InstanceMode = flags.String("mode", "Normal", "Optional. Mode of udb instance. Normal or HA, HA means high-availability. Both the normal and high-availability versions can create master-slave synchronization for data redundancy and read/write separation. The high-availability version provides a dual-master hot standby architecture to avoid database unavailability due to downtime or hardware failure. One more thing. It does better job for master-slave synchronization and disaster recovery using the InnoDB engine") + req.VPCId = flags.String("vpc-id", "", "Optional. Resource ID of VPC which the UDB to create belong to. See 'ucloud vpc list'") + req.SubnetId = flags.String("subnet-id", "", "Optional. Resource ID of subnet that the UDB to create belong to. See 'ucloud subnet list'") + flags.BoolVar(&async, "async", false, "Optional. Do not wait for the long-running operation to finish.") + bindChargeType(req, flags) + bindQuantity(req, flags) + + flags.SetFlagValues("version", dbVersionList...) + flags.SetFlagValues("disk-type", dbDiskTypeList...) + flags.SetFlagValuesFunc("vpc-id", func() []string { + return getAllVPCIdNames(*req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("subnet-id", func() []string { + return getAllSubnetIDNames(*req.VPCId, *req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("conf-id", func() []string { + return getConfIDList(*req.DBTypeId, *req.ProjectId, *req.Region, *req.Zone) + }) + + cmd.MarkFlagRequired("version") + cmd.MarkFlagRequired("name") + cmd.MarkFlagRequired("password") + cmd.MarkFlagRequired("conf-id") + return cmd +} + +// UDBMysqlRow 表格行 +type UDBMysqlRow struct { + Name string + ResourceID string + Role string + Status string + Config string + Mode string + DiskType string + IP string + Group string + Zone string + VPC string + Subnet string + // CreateTime string +} + +// NewCmdUDBList ucloud udb list +func NewCmdUDBList(out io.Writer) *cobra.Command { + req := base.BizClient.NewDescribeUDBInstanceRequest() + cmd := &cobra.Command{ + Use: "list", + Short: "List MySQL instances", + Long: "List MySQL instances", + Run: func(c *cobra.Command, args []string) { + if *req.DBId != "" { + *req.DBId = base.PickResourceID(*req.DBId) + } + resp, err := base.BizClient.DescribeUDBInstance(req) + if err != nil { + base.HandleError(err) + return + } + list := []UDBMysqlRow{} + for _, ins := range resp.DataSet { + row := UDBMysqlRow{} + row.Name = ins.Name + row.Zone = ins.Zone + row.Role = ins.Role + row.ResourceID = ins.DBId + row.Group = ins.Tag + row.VPC = ins.VPCId + row.Subnet = ins.SubnetId + row.IP = ins.VirtualIP + row.Mode = ins.InstanceMode + row.DiskType = ins.InstanceType + row.Status = ins.State + row.Config = fmt.Sprintf("%s|%dG|%dG", ins.DBTypeId, ins.MemoryLimit/1000, ins.DiskSpace) + list = append(list, row) + for _, slave := range ins.DataSet { + row := UDBMysqlRow{} + row.Name = slave.Name + row.Zone = slave.Zone + row.Role = fmt.Sprintf("\u2b91 %s", slave.Role) + row.ResourceID = slave.DBId + row.Group = slave.Tag + row.VPC = slave.VPCId + row.Subnet = slave.SubnetId + row.IP = slave.VirtualIP + row.Mode = slave.InstanceMode + row.DiskType = slave.InstanceType + row.Config = fmt.Sprintf("%s|%dG|%dG", slave.DBTypeId, slave.MemoryLimit/1000, slave.DiskSpace) + row.Status = slave.State + list = append(list, row) + } + } + base.PrintList(list, out) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + req.DBId = flags.String("udb-id", "", "Optional. List the specified mysql") + bindProjectID(req, flags) + bindRegion(req, flags) + bindZone(req, flags) + bindLimit(req, flags) + bindOffset(req, flags) + req.IncludeSlaves = flags.Bool("include-slaves", false, "Optional. When specifying the udb-id, whether to display its slaves together. Accept values:true, false") + req.ClassType = sdk.String("sql") + + flags.SetFlagValues("include-slaves", "true", "false") + flags.SetFlagValuesFunc("udb-id", func() []string { + return getUDBIDList(nil, "sql", *req.ProjectId, *req.Region, *req.Zone) + }) + + return cmd +} + +// NewCmdUDBDelete ucloud udb delete +func NewCmdUDBDelete(out io.Writer) *cobra.Command { + var idNames []string + var yes bool + req := base.BizClient.NewDeleteUDBInstanceRequest() + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete MySQL instances by udb-id", + Long: "Delete MySQL instances by udb-id", + Run: func(c *cobra.Command, args []string) { + ok := base.Confirm(yes, "Are you sure you want to delete the udb(s)?") + if !ok { + return + } + for _, idname := range idNames { + id := base.PickResourceID(idname) + any, err := describeUdbByID(id, nil) + if err != nil { + base.HandleError(err) + continue + } + req.DBId = &id + ins, ok := any.(*udb.UDBInstanceSet) + if ok && ins.State == status.UDB_RUNNING { + stopReq := base.BizClient.NewStopUDBInstanceRequest() + stopReq.ProjectId = req.ProjectId + stopReq.Region = req.Region + stopReq.Zone = req.Zone + stopReq.DBId = req.DBId + stopUdbIns(stopReq, false, out) + } + _, err = base.BizClient.DeleteUDBInstance(req) + if err != nil { + base.HandleError(err) + continue + } + fmt.Fprintf(out, "udb[%s] deleted\n", idname) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&idNames, "udb-id", nil, "Required. Resource ID of UDB instances to delete") + bindRegion(req, flags) + bindZone(req, flags) + bindProjectID(req, flags) + flags.BoolVarP(&yes, "yes", "y", false, "Optional. Do not prompt for confirmation.") + + cmd.MarkFlagRequired("udb-id") + flags.SetFlagValuesFunc("udb-id", func() []string { + return getUDBIDList(nil, "", *req.ProjectId, *req.Region, *req.Zone) + }) + return cmd +} + +// NewCmdUDBStop ucloud udb stop +func NewCmdUDBStop(out io.Writer) *cobra.Command { + var idNames []string + var async bool + req := base.BizClient.NewStopUDBInstanceRequest() + cmd := &cobra.Command{ + Use: "stop", + Short: "Stop MySQL instances by udb-id", + Long: "Stop MySQL instances by udb-id", + Run: func(c *cobra.Command, args []string) { + for _, idname := range idNames { + req.DBId = sdk.String(base.PickResourceID(idname)) + stopUdbIns(req, async, out) + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&idNames, "udb-id", nil, "Required. Resource ID of UDB instances to stop") + bindRegion(req, flags) + bindZone(req, flags) + bindProjectID(req, flags) + req.ForceToKill = flags.Bool("force", false, "Optional. Stop UDB instances by force or not") + flags.BoolVarP(&async, "async", "a", false, "Optional. Do not wait for the long-running operation to finish.") + + cmd.MarkFlagRequired("udb-id") + + flags.SetFlagValues("force", "true", "false") + flags.SetFlagValuesFunc("udb-id", func() []string { + return getUDBIDList([]string{status.UDB_RUNNING}, "", *req.ProjectId, *req.Region, *req.Zone) + }) + + return cmd +} + +// NewCmdUDBStart ucloud udb start +func NewCmdUDBStart(out io.Writer) *cobra.Command { + var async bool + var idNames []string + req := base.BizClient.NewStartUDBInstanceRequest() + cmd := &cobra.Command{ + Use: "start", + Short: "Start MySQL instances by udb-id", + Long: "Start MySQL instances by udb-id", + Run: func(c *cobra.Command, args []string) { + for _, idname := range idNames { + id := base.PickResourceID(idname) + req.DBId = &id + _, err := base.BizClient.StartUDBInstance(req) + if err != nil { + base.HandleError(err) + continue + } + if async { + fmt.Fprintf(out, "udb[%s] is starting\n", idname) + } else { + text := fmt.Sprintf("udb[%s] is starting", idname) + poller.Spoll(*req.DBId, text, []string{status.UDB_RUNNING, status.UDB_FAIL}) + } + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&idNames, "udb-id", nil, "Required. Resource ID of UDB instances to start") + bindRegion(req, flags) + bindZone(req, flags) + bindProjectID(req, flags) + flags.BoolVarP(&async, "async", "a", false, "Optional. Do not wait for the long-running operation to finish.") + + cmd.MarkFlagRequired("udb-id") + + flags.SetFlagValuesFunc("udb-id", func() []string { + return getUDBIDList([]string{status.UDB_SHUTOFF}, "", *req.ProjectId, *req.Region, *req.Zone) + }) + return cmd +} + +// NewCmdUDBRestart ucloud udb restart +func NewCmdUDBRestart(out io.Writer) *cobra.Command { + var async bool + var idNames []string + req := base.BizClient.NewRestartUDBInstanceRequest() + cmd := &cobra.Command{ + Use: "restart", + Short: "Restart MySQL instances by udb-id", + Long: "Restart MySQL instances by udb-id", + Run: func(c *cobra.Command, args []string) { + for _, idname := range idNames { + id := base.PickResourceID(idname) + req.DBId = &id + _, err := base.BizClient.RestartUDBInstance(req) + if err != nil { + base.HandleError(err) + continue + } + if async { + fmt.Fprintf(out, "udb[%s] is restarting\n", idname) + } else { + text := fmt.Sprintf("udb[%s] is restarting", idname) + poller.Spoll(*req.DBId, text, []string{status.UDB_RUNNING, status.UDB_FAIL}) + } + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&idNames, "udb-id", nil, "Required. Resource ID of UDB instances to restart") + bindRegion(req, flags) + bindZone(req, flags) + bindProjectID(req, flags) + flags.BoolVarP(&async, "async", "a", false, "Optional. Do not wait for the long-running operation to finish.") + + cmd.MarkFlagRequired("udb-id") + flags.SetFlagValuesFunc("udb-id", func() []string { + return getUDBIDList(nil, "", *req.ProjectId, *req.Region, *req.Zone) + }) + return cmd +} + +// NewCmdUDBResize ucloud udb resize +func NewCmdUDBResize(out io.Writer) *cobra.Command { + var diskTypes = []string{"normal", "sata_ssd", "pcie_ssd", "normal_volume", "sata_ssd_volume", "pcie_ssd_volume"} + var async, yes bool + var idNames []string + var memory, disk int + var diskType string + req := base.BizClient.NewResizeUDBInstanceRequest() + cmd := &cobra.Command{ + Use: "resize", + Short: "Reszie MySQL instances, such as memory size, disk size and disk type", + Long: "Reszie MySQL instances, such as memory size, disk size and disk type", + Run: func(c *cobra.Command, args []string) { + if diskType != "" { + switch diskType { + case "normal": + req.InstanceType = sdk.String("Normal") + case "sata_ssd": + req.InstanceType = sdk.String("SATA_SSD") + case "pcie_ssd": + req.InstanceType = sdk.String("PCIE_SSD") + case "normal_volume": + req.InstanceType = sdk.String("Normal_Volume") + case "sata_ssd_volume": + req.InstanceType = sdk.String("SATA_SSD_Volume") + case "pcie_ssd_volume": + req.InstanceType = sdk.String("PCIE_SSD_Volume") + default: + req.InstanceType = &diskType + } + } + + for _, idname := range idNames { + id := base.PickResourceID(idname) + req.DBId = &id + any, err := describeUdbByID(id, nil) + if err != nil { + base.HandleError(err) + continue + } + + ins, ok := any.(*udb.UDBInstanceSet) + if !ok { + continue + } + + if memory != 0 { + req.MemoryLimit = sdk.Int(memory * 1000) + } else { + req.MemoryLimit = &ins.MemoryLimit + } + if disk != 0 { + req.DiskSpace = &disk + } else { + req.DiskSpace = &ins.DiskSpace + } + + if ins.State == status.UDB_RUNNING { + ok := base.Confirm(yes, fmt.Sprintf("Need to shut down udb[%s] before upgrading, whether to continue?", idname)) + if !ok { + continue + } + stopReq := base.BizClient.NewStopUDBInstanceRequest() + stopReq.ProjectId = req.ProjectId + stopReq.Region = req.Region + stopReq.Zone = req.Zone + stopReq.DBId = req.DBId + stopUdbIns(stopReq, false, out) + } + _, err = base.BizClient.ResizeUDBInstance(req) + if err != nil { + base.HandleError(err) + continue + } + if async { + fmt.Fprintf(out, "udb[%s] is resizing\n", idname) + } else { + text := fmt.Sprintf("udb[%s] is resizing", idname) + poller.Spoll(*req.DBId, text, []string{status.UDB_RUNNING, status.UDB_SHUTOFF, status.UDB_FAIL, status.UDB_UPGRADE_FAIL}) + } + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&idNames, "udb-id", nil, "Required. Resource ID of UDB instances to restart") + bindRegion(req, flags) + bindZone(req, flags) + bindProjectID(req, flags) + flags.IntVar(&memory, "memory-size-gb", 0, "Optional. Memory size of udb instance. From 1 to 128. Unit GB") + flags.IntVar(&disk, "disk-size-gb", 0, "Optional. Disk size of udb instance. From 20 to 3000 according to memory size. Unit GB. Step 10GB") + flags.StringVar(&diskType, "disk-type", "", fmt.Sprintf("Optional. Disk type of udb instance. Accept values:%s", strings.Join(diskTypes, ", "))) + req.StartAfterUpgrade = flags.Bool("start-after-upgrade", true, "Optional. Automatic start the UDB instances after upgrade") + flags.BoolVarP(&async, "async", "a", false, "Optional. Do not wait for the long-running operation to finish") + flags.BoolVarP(&yes, "yes", "y", false, "Optional. Do not prompt for confirmation") + + flags.SetFlagValues("disk-type", diskTypes...) + flags.SetFlagValuesFunc("udb-id", func() []string { + return getUDBIDList(nil, "", *req.ProjectId, *req.Region, *req.Zone) + }) + + cmd.MarkFlagRequired("udb-id") + + return cmd +} + +// NewCmdUDBResetPassword ucloud udb reset-password +func NewCmdUDBResetPassword(out io.Writer) *cobra.Command { + var idNames []string + req := base.BizClient.NewModifyUDBInstancePasswordRequest() + cmd := &cobra.Command{ + Use: "reset-password", + Short: "Reset password of MySQL instances", + Long: "Reset password of MySQL instances", + Run: func(c *cobra.Command, args []string) { + for _, idname := range idNames { + id := base.PickResourceID(idname) + req.DBId = &id + _, err := base.BizClient.ModifyUDBInstancePassword(req) + if err != nil { + base.HandleError(err) + continue + } + fmt.Fprintf(out, "udb[%s]'s password modified\n", idname) + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&idNames, "udb-id", nil, "Required. Resource ID of UDB instances to reset password") + req.Password = flags.String("password", "", "Required. New password") + bindProjectID(req, flags) + bindRegion(req, flags) + bindZone(req, flags) + + cmd.MarkFlagRequired("udb-id") + cmd.MarkFlagRequired("password") + + flags.SetFlagValuesFunc("udb-id", func() []string { + return getUDBIDList(nil, "", *req.ProjectId, *req.Region, *req.Zone) + }) + + return cmd +} + +// NewCmdUDBRestore ucloud udb restore +func NewCmdUDBRestore(out io.Writer) *cobra.Command { + var datetime, diskType string + var async bool + req := base.BizClient.NewCreateUDBInstanceByRecoveryRequest() + cmd := &cobra.Command{ + Use: "restore", + Short: "Create MySQL instance and restore the newly created db to the specified DB at a specified point in time", + Long: "Create MySQL instance and restore the newly created db to the specified DB at a specified point in time", + Run: func(c *cobra.Command, args []string) { + t, err := time.Parse(time.RFC3339, datetime) + if err != nil { + base.HandleError(err) + return + } + req.RecoveryTime = sdk.Int(int(t.Unix())) + req.SrcDBId = sdk.String(base.PickResourceID(*req.SrcDBId)) + if diskType == "" { + any, err := describeUdbByID(*req.SrcDBId, nil) + if err != nil { + base.HandleError(err) + return + } + ins, ok := any.(*udb.UDBInstanceSet) + if !ok { + fmt.Fprintln(out, fmt.Sprintf("fetch udb[%s] instance", *req.SrcDBId)) + } + req.UseSSD = &ins.UseSSD + } else if diskType == "normal" { + req.UseSSD = sdk.Bool(false) + } else if diskType == "ssd" { + req.UseSSD = sdk.Bool(true) + } + resp, err := base.BizClient.CreateUDBInstanceByRecovery(req) + if async { + fmt.Fprintf(out, "udb[%s] is restorting from udb[%s] at time point %s", resp.DBId, *req.SrcDBId, datetime) + } else { + text := fmt.Sprintf("udb[%s] is restorting from udb[%s] at time point %s", resp.DBId, *req.SrcDBId, datetime) + poller.Spoll(resp.DBId, text, []string{status.UDB_RUNNING, status.UDB_RECOVER_FAIL, status.UDB_FAIL}) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + req.Name = flags.String("name", "", "Required. Name of UDB instance to create") + req.SrcDBId = flags.String("src-udb-id", "", "Required. Resource ID of source UDB") + flags.StringVar(&datetime, "restore-to-time", "", "Required. The date and time to restore the DB to. Value must be a time in Universal Coordinated Time (UTC) format.Example: 2019-02-23T23:45:00Z") + bindRegion(req, flags) + bindZone(req, flags) + bindProjectID(req, flags) + flags.StringVar(&diskType, "disk-type", "", "Optional. Disk type. The default is to be consistent with the source database. Accept values: normal, ssd") + bindChargeType(req, flags) + bindQuantity(req, flags) + flags.BoolVarP(&async, "async", "a", false, "Optional. Do not wait for the long-running operation to finish") + + cmd.MarkFlagRequired("name") + cmd.MarkFlagRequired("src-udb-id") + cmd.MarkFlagRequired("restore-to-time") + + flags.SetFlagValues("disk-type", "noraml", "ssd") + flags.SetFlagValuesFunc("src-udb-id", func() []string { + return getUDBIDList(nil, "sql", *req.ProjectId, *req.Region, *req.Zone) + }) + + return cmd +} + +// NewCmdUDBCreateSlave ucloud udb create-slave +func NewCmdUDBCreateSlave(out io.Writer) *cobra.Command { + var diskType string + var async bool + req := base.BizClient.NewCreateUDBSlaveRequest() + cmd := &cobra.Command{ + Use: "create-slave", + Short: "Create slave database", + Long: "Create slave database", + Run: func(c *cobra.Command, args []string) { + *req.SrcId = base.PickResourceID(*req.SrcId) + switch diskType { + case "normal": + req.UseSSD = sdk.Bool(false) + case "sata_ssd": + req.UseSSD = sdk.Bool(true) + req.SSDType = sdk.String("SATA") + case "pcie_ssd": + req.UseSSD = sdk.Bool(true) + req.SSDType = sdk.String("PCI-E") + } + *req.MemoryLimit *= 1000 + resp, err := base.BizClient.CreateUDBSlave(req) + if err != nil { + base.HandleError(err) + return + } + if async { + fmt.Fprintf(out, "udb[%s] is initializing\n", resp.DBId) + } else { + poller.Spoll(resp.DBId, fmt.Sprintf("udb[%s] is initializing", resp.DBId), []string{status.UDB_RUNNING, status.UDB_FAIL}) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + req.SrcId = flags.String("master-udb-id", "", "Required. Resource ID of master UDB instance") + req.Name = flags.String("name", "", "Required. Name of the slave DB to create") + req.Port = flags.Int("port", 3306, "Optional. Port of the slave db service") + bindRegion(req, flags) + bindZone(req, flags) + bindProjectID(req, flags) + flags.StringVar(&diskType, "disk-type", "Normal", fmt.Sprintf("Optional. Setting this flag means using SSD disk. Accept values: %s", strings.Join(dbDiskTypeList, ", "))) + req.MemoryLimit = flags.Int("memory-size-gb", 1, "Optional. Memory size of udb instance. From 1 to 128. Unit GB") + flags.BoolVar(&async, "async", false, "Optional. Do not wait for the long-running operation to finish") + req.IsLock = flags.Bool("is-lock", false, "Optional. Lock master DB or not") + + cmd.MarkFlagRequired("master-udb-id") + cmd.MarkFlagRequired("name") + + flags.SetFlagValues("disk-type", dbDiskTypeList...) + flags.SetFlagValuesFunc("master-udb-id", func() []string { + return getUDBIDList(nil, "", *req.ProjectId, *req.Region, *req.Zone) + }) + return cmd +} + +// NewCmdUDBPromoteSlave ucloud udb promote-slave +func NewCmdUDBPromoteSlave(out io.Writer) *cobra.Command { + var ids []string + req := base.BizClient.NewPromoteUDBSlaveRequest() + cmd := &cobra.Command{ + Use: "promote-slave", + Short: "Promote slave db to master", + Long: "Promote slave db to master", + Run: func(c *cobra.Command, args []string) { + for _, id := range ids { + req.DBId = sdk.String(id) + _, err := base.BizClient.PromoteUDBSlave(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintf(out, "udb[%s] was promoted\n", *req.DBId) + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&ids, "udb-id", nil, "Required. Resource ID of slave db to promote") + req.IsForce = flags.Bool("is-force", false, "Optional. Force to promote slave db or not. If the slave db falls behind, the force promote may lose some data") + bindRegion(req, flags) + bindZone(req, flags) + bindProjectID(req, flags) + + cmd.MarkFlagRequired("udb-id") + + return cmd +} + +// NewCmdUDBPromoteToHA ucloud udb promote-to-ha 低频操作 暂不开放 +func NewCmdUDBPromoteToHA(out io.Writer) *cobra.Command { + var idNames []string + req := base.BizClient.NewPromoteUDBInstanceToHARequest() + cmd := &cobra.Command{ + Use: "promote-to-ha", + Short: "Promote db of normal mode to high availability db. ", + Long: "Promote db of normal mode to high availability db", + Run: func(c *cobra.Command, args []string) { + for _, idname := range idNames { + id := base.PickResourceID(idname) + req.DBId = &id + _, err := base.BizClient.PromoteUDBInstanceToHA(req) + if err != nil { + base.HandleError(err) + continue + } + poller.Spoll(id, fmt.Sprintf("udb[%s] is synchronizing data", id), []string{status.UDB_TOBE_SWITCH, status.UDB_FAIL}) + any, err := describeUdbByID(id, nil) + if err != nil { + fmt.Fprintf(out, "udb[%s] promoted failed, please contact technical support; %v\n", idname, err) + continue + } + ins, ok := any.(*udb.UDBInstanceSet) + if !ok { + fmt.Fprintf(out, "udb[%s] promoted failed, please contact technical support. \n", idname) + continue + } + if ins.State != status.UDB_TOBE_SWITCH { + fmt.Fprintf(out, "udb[%s] promoted failed, please contact technical support. udb[%s]'s status:%s\n", idname, idname, ins.State) + continue + } + switchReq := base.BizClient.NewSwitchUDBInstanceToHARequest() + switchReq.DBId = &id + switchReq.Region = req.Region + switchReq.ProjectId = req.ProjectId + switchReq.ChargeType = &ins.ChargeType + switchReq.Quantity = sdk.String("0") + switchReq.Zone = &base.ConfigIns.Zone + switchResp, err := base.BizClient.SwitchUDBInstanceToHA(switchReq) + if err != nil { + fmt.Fprintf(out, "udb[%s] promoted failed, please contact technical support; %v\n", idname, err) + continue + } + poller.Spoll(switchResp.DBId, fmt.Sprintf("udb[%s] is switching to high availability mode", switchResp.DBId), []string{status.UDB_RUNNING, status.UDB_FAIL}) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + bindRegion(req, flags) + bindProjectID(req, flags) + flags.StringSliceVar(&idNames, "udb-id", nil, "Required. Resource ID of UDB instances to be promoted as high availability mode") + + cmd.MarkFlagRequired("udb-id") + flags.SetFlagValuesFunc("udb-id", func() []string { + return getUDBIDList(nil, "", *req.ProjectId, *req.Region, "") + }) + return cmd +} + +func stopUdbIns(req *udb.StopUDBInstanceRequest, async bool, out io.Writer) { + _, err := base.BizClient.StopUDBInstance(req) + if err != nil { + base.HandleError(err) + return + } + text := fmt.Sprintf("udb[%s] is stopping", *req.DBId) + if async { + fmt.Fprintln(out, text) + } else { + poller.Spoll(*req.DBId, text, []string{status.UDB_SHUTOFF, status.UDB_FAIL}) + } +} + +func getUDBIDList(states []string, dbType, project, region, zone string) []string { + udbs, err := getUDBList(states, dbType, project, region, zone) + if err != nil { + return nil + } + list := []string{} + for _, db := range udbs { + list = append(list, fmt.Sprintf("%s/%s", db.DBId, db.Name)) + } + return list +} + +func getUDBList(states []string, dbType, project, region, zone string) ([]udb.UDBInstanceSet, error) { + req := base.BizClient.NewDescribeUDBInstanceRequest() + if dbType == "" { + dbType = "sql" + } + req.ClassType = &dbType + req.ProjectId = &project + req.Region = ®ion + req.Zone = &zone + list := []udb.UDBInstanceSet{} + for offset, limit := 0, 50; ; offset += limit { + req.Offset = sdk.Int(offset) + req.Limit = sdk.Int(limit) + resp, err := base.BizClient.DescribeUDBInstance(req) + if err != nil { + return nil, err + } + for _, ins := range resp.DataSet { + if states != nil { + for _, s := range states { + if s == ins.State { + list = append(list, ins) + } + } + } else { + list = append(list, ins) + } + } + if offset+limit >= resp.TotalCount { + break + } + } + return list, nil +} + +func describeUdbByID(udbID string, commonBase *request.CommonBase) (interface{}, error) { + req := base.BizClient.NewDescribeUDBInstanceRequest() + if commonBase != nil { + req.CommonBase = *commonBase + } + req.DBId = sdk.String(udbID) + resp, err := base.BizClient.DescribeUDBInstance(req) + if err != nil { + return nil, err + } + if len(resp.DataSet) < 1 { + return nil, fmt.Errorf("udb[%s] may not exist", udbID) + } + return &resp.DataSet[0], nil +} diff --git a/cmd/pathx.go b/cmd/pathx.go new file mode 100644 index 0000000000..8f3d2e4f98 --- /dev/null +++ b/cmd/pathx.go @@ -0,0 +1,1464 @@ +// Copyright © 2018 NAME HERE tony.li@ucloud.cn +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "io" + "strconv" + "strings" + + ppathx "github.com/ucloud/ucloud-sdk-go/private/services/pathx" + "github.com/ucloud/ucloud-sdk-go/services/pathx" + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + uerr "github.com/ucloud/ucloud-sdk-go/ucloud/error" + + "github.com/spf13/cobra" + "github.com/ucloud/ucloud-cli/base" + "github.com/ucloud/ucloud-cli/ux" +) + +// NewCmdPathx ucloud pathx +func NewCmdPathx() *cobra.Command { + cmd := &cobra.Command{ + Use: "pathx", + Short: "Manipulate uga and upath instances", + Long: "Manipulate uga and upath instances", + } + out := base.Cxt.GetWriter() + cmd.AddCommand(NewCmdUGA()) + cmd.AddCommand(NewCmdUpath()) + cmd.AddCommand(NewCmdUGA3Create(out)) + cmd.AddCommand(NewCmdUGA3Delete(out)) + cmd.AddCommand(NewCmdUGA3Modify(out)) + cmd.AddCommand(NewCmdUGA3List(out)) + cmd.AddCommand(NewCmdPathxPrice(out)) + cmd.AddCommand(NewCmdPathxArea(out)) + + return cmd +} + +// create pathx instance +func NewCmdUGA3Create(out io.Writer) *cobra.Command { + createPathxReq := base.BizClient.NewCreateUGA3InstanceRequest() + createPathxPortReq := base.BizClient.NewCreateUGA3PortRequest() + spinner := ux.NewDotSpinner(out) + var ports, originPorts []string + protocol := "tcp" + createCmd := &cobra.Command{ + Use: "create", + Short: "Create the pathx resource and port", + Long: "Create global unified access acceleration configuration item", + Example: "ucloud pathx create --bandwidth 10 --area-code DXB" + + "--charge-type Month --quantity 4 --accel Global --origin-ip 110.111.111.111" + + "--protocol TCP --port 30654 --origin-port 30564", + Run: func(cmd *cobra.Command, args []string) { + spinner.Start("The pathx resource creating") + if *createPathxReq.OriginIPList == "" && *createPathxReq.OriginDomain == "" { + spinner.Fail(fmt.Errorf("The origin-ip and origin-domain cannot be empty at the same time")) + return + } + portIntList := make([]int, 0) + originPortIntList := make([]int, 0) + if len(ports) > 0 || len(originPorts) > 0 { + if len(ports) == 0 { + spinner.Fail(fmt.Errorf("The port cannot be empty.")) + return + } else if len(originPorts) == 0 { + spinner.Fail(fmt.Errorf("The origin-port cannot be empty.")) + return + } + if strings.EqualFold(protocol, "UDP") { + spinner.Fail(fmt.Errorf("The udp protocol is temporarily not supported for create")) + return + } else if !strings.EqualFold(protocol, "TCP") && + !strings.EqualFold(protocol, "UDP") { + spinner.Fail(fmt.Errorf("The value of protocol input error,please input 'TCP' or 'UDP',and the value entered is not case sensitive")) + return + } + tcpPortList, err := formatPortList(ports) + if err != nil { + spinner.Fail(err) + return + } + // tcpPorts convert to []int + for _, tcpPort := range tcpPortList { + port, _ := strconv.Atoi(tcpPort) + portIntList = append(portIntList, port) + } + rsTcpPortList, err := formatPortList(originPorts) + if err != nil { + spinner.Fail(err) + return + } + // rsTcpPorts convert to []int + for _, rsTcpPort := range rsTcpPortList { + rsPort, _ := strconv.Atoi(rsTcpPort) + originPortIntList = append(originPortIntList, rsPort) + } + if len(portIntList) != len(originPortIntList) { + spinner.Fail(fmt.Errorf("The number of port must be consistent with the number of origin-port.")) + return + } else if len(portIntList) >= 10 { + spinner.Fail(fmt.Errorf("The number of port cannot greater than or equals to 10")) + return + } + } + if strings.EqualFold(*createPathxReq.ChargeType, "Month") { + *createPathxReq.Quantity = 0 + } else if *createPathxReq.Quantity <= 0 { + spinner.Fail(fmt.Errorf("If the value of charge-type is 'Year' or 'Hour',the value of quantity must be greater than 0")) + return + } + + switch strings.ToLower(*createPathxReq.ChargeType) { + case "hour": + *createPathxReq.ChargeType = "Dynamic" + case "month": + *createPathxReq.ChargeType = "Month" + case "year": + *createPathxReq.ChargeType = "Year" + } + // post create pathx resource + createUGA3InstanceResp, err := base.BizClient.CreateUGA3Instance(createPathxReq) + if err != nil { + spinner.Fail(err) + return + } + if createUGA3InstanceResp == nil || createUGA3InstanceResp.InstanceId == "" || + &createUGA3InstanceResp.InstanceId == nil { + spinner.Fail(fmt.Errorf("An unknown error occurred and could not be created successfully.")) + return + } + spinner.Stop() + + // CreatePathxPort + if len(portIntList) > 0 && len(originPortIntList) > 0 { + createPathxPortReq.InstanceId = &createUGA3InstanceResp.InstanceId + createPathxPortReq.SetRegionRef(createPathxReq.GetRegionRef()) + createPathxPortReq.SetProjectIdRef(createPathxReq.GetProjectIdRef()) + createPathxPortReq.SetZoneRef(createPathxReq.GetZoneRef()) + spinner.Start("The pathx port creating") + // Temporary support tcp protocol + if strings.EqualFold(protocol, "TCP") { + createPathxPortReq.TCP = portIntList + createPathxPortReq.TCPRS = originPortIntList + } + _, err := base.BizClient.CreateUGA3Port(createPathxPortReq) + if err != nil { + spinner.Fail(err) + return + } + spinner.Stop() + } + + fmt.Fprintf(out, "The resource is created, and the resource ID is: %s\n", createUGA3InstanceResp.InstanceId) + }, + } + flags := createCmd.Flags() + flags.SortFlags = false + + bindProjectID(createPathxReq, flags) + bindRegion(createPathxReq, flags) + bindZone(createPathxReq, flags) + + createPathxReq.Bandwidth = flags.String("bandwidth", "0", + "Required. Shared bandwidth of the resource") + flags.String("area-code", "", + "Optional. When it is empty,the nearest zone will be selected based on the origin-domain and origin-ip. "+ + "Acceptable values:'BKK'(曼谷),'DXB'(迪拜),'FRA'(法兰克福),'SGN'(胡志明市),'HKG'(香港),'CGK'(雅加达),'LOS'(拉各斯),'LHR'(伦敦),'LAX'(洛杉矶),"+ + "'MNL'(马尼拉),'DME'(莫斯科),'BOM'(孟买),'MSP'(圣保罗),'ICN'(首尔),'PVG'(上海),'SIN'(新加坡),'NRT'(东京),'IAD'(华盛顿),'TPE'(台北)") + + createPathxReq.ChargeType = flags.String("charge-type", "", + "Optional. Payment method,its value is not case sensitive,acceptable values:'Year',pay yearly;'Month',pay monthly;'Hour', pay hourly") + createPathxReq.Quantity = flags.Int("quantity", 1, + "Optional. The duration of the pathx resource, the value cannot be less than or equal to 0. N years/months") + createPathxReq.AccelerationArea = flags.String("accel", "", + "Optional. The default value is 'Global'(全球). "+ + "Other acceptable values:'AP'(亚太);'EU'(欧洲);'ME'(中东);'OA'(大洋洲);'AF'(非洲);'NA'(北美洲);'SA'(南美洲)") + createPathxReq.OriginIPList = flags.String("origin-ip", "", + "Optional. But when the origin-domain is empty,it cannot be empty. If multiple values exist,please split by ','. For example '0.0.0.0,110.110.100.100'") + createPathxReq.OriginDomain = flags.String("origin-domain", "", + "Optional. But when the origin-ip is empty,it cannot be empty") + + flags.StringSliceVar(&ports, "port", nil, + "Optional. Disable 65123 port,the port can be multiple,please split by ',' for example 80,3000-3010. "+ + "The number of port must be consistent with the number of origin-port,and the number cannot greater than or equals to 10") + flags.StringSliceVar(&originPorts, "origin-port", nil, + "Optional. The origin-port can be multiple,please split by ',' for example 80,3000-3010."+ + "The number of origin-port must be consistent with the number of port") + flags.StringVar(&protocol, "protocol", "TCP", "Its values can be TCP and UDP, but currently only supports TCP") + + createCmd.MarkFlagRequired("bandwidth") + flags.SetFlagValues("area-code", + "BKK", "DXB", "FRA", "SGN", "HKG", "CGK", "LOS", "LHR", "LAX", "MNL", "DME", "BOM", "MSP", "ICN", "PVG", "SIN", "NRT", "IAD", "TPE") + flags.SetFlagValues("charge-type", "Month", "Year", "Hour") + flags.SetFlagValues("accel", "Global", "AP", "EU", "ME", "OA", "AF", "NA", "SA") + flags.SetFlagValues("protocol", "TCP", "UDP") + return createCmd +} + +// delete pathx instance +func NewCmdUGA3Delete(out io.Writer) *cobra.Command { + deleteUga3Req := base.BizClient.NewDeleteUGA3InstanceRequest() + deleteUga3PortReq := base.BizClient.NewDeleteUGA3PortRequest() + spinner := ux.NewDotSpinner(out) + var yes *bool + var instanceId string + removeCmd := &cobra.Command{ + Use: "delete", + Short: "Delete the pathx resource and port", + Long: "Delete the pathx resource and port", + Example: "ucloud pathx delete --id uga3-xxx", + Run: func(cmd *cobra.Command, args []string) { + if !*yes { + sure, err := ux.Prompt("Are you sure you want to delete this resource ?") + if err != nil { + base.Cxt.Println(err) + return + } + if !sure { + return + } + } + spinner.Start(fmt.Sprintf("Starting delete the pathx[%s] resource port", instanceId)) + deleteUga3PortReq.InstanceId = &instanceId + _, deletePortErr := base.BizClient.DeleteUGA3Port(deleteUga3PortReq) + if deletePortErr != nil { + spinner.Fail(deletePortErr) + return + } + spinner.Stop() + + spinner.Start(fmt.Sprintf("Starting delete the pathx[%s] resource", instanceId)) + deleteUga3Req.InstanceId = &instanceId + deleteUga3Req.SetProjectIdRef(deleteUga3PortReq.GetProjectIdRef()) + deleteUga3Req.SetRegionRef(deleteUga3PortReq.GetRegionRef()) + deleteUga3Req.SetZoneRef(deleteUga3PortReq.GetZoneRef()) + _, err := base.BizClient.DeleteUGA3Instance(deleteUga3Req) + if err != nil { + spinner.Fail(err) + return + } + spinner.Stop() + }, + } + flags := removeCmd.Flags() + flags.SortFlags = false + flags.StringVar(&instanceId, "id", "", + "Required. It is the resource ID of pathx, and the deletion will be performed according to this") + + bindProjectID(deleteUga3PortReq, flags) + bindRegion(deleteUga3PortReq, flags) + bindZone(deleteUga3PortReq, flags) + + removeCmd.MarkFlagRequired("id") + yes = removeCmd.Flags().BoolP("yes", "y", false, "Optional. Do not prompt for confirmation.") + flags.SetFlagValuesFunc("id", func() []string { + return getPathxList(*deleteUga3PortReq.ProjectId, *deleteUga3PortReq.Region, *deleteUga3PortReq.Zone) + }) + return removeCmd +} + +func getPathxList(project, region, zone string) []string { + getInstanceReq := base.BizClient.NewDescribeUGA3InstanceRequest() + getInstanceReq.ProjectId = sdk.String(project) + getInstanceReq.Region = sdk.String(region) + getInstanceReq.Zone = sdk.String(zone) + getInstanceReq.Limit = sdk.Int(50) + resp, err := base.BizClient.DescribeUGA3Instance(getInstanceReq) + if err != nil { + base.HandleError(err) + return nil + } + list := make([]string, 0) + for _, item := range resp.ForwardInstanceInfos { + list = append(list, item.InstanceId) + } + return list +} + +// modify UGA3 instance +func NewCmdUGA3Modify(out io.Writer) *cobra.Command { + modifyBandwidthReq := base.BizClient.NewModifyUGA3BandwidthRequest() + modifyOriginInfoReq := base.BizClient.NewModifyUGA3OriginInfoRequest() + modifyInstanceReq := base.BizClient.NewModifyUGA3InstanceRequest() + modifyPortReq := base.BizClient.NewModifyUGA3PortRequest() + + spinner := ux.NewDotSpinner(out) + var tcpPorts, rsTcpPorts []string + var instanceId string + protocol := "TCP" + modifyCmd := &cobra.Command{ + Use: "modify", + Short: "Modify the pathx associated information. Example bandwidth or origin information or resource information", + Long: "Support modify bandwidth,origin information,resource information,port", + Example: "ucloud pathx modify --id uga3-xxx --bandwidth 1 --origin-ip 127.0.0.1 --name Pathx测试 --remark 加速资源 --protocol TCP --port 30010 --origin-port 39999", + Run: func(cmd *cobra.Command, args []string) { + modifyBandwidthReq.InstanceId = &instanceId + modifyInstanceReq.InstanceId = &instanceId + modifyOriginInfoReq.InstanceId = &instanceId + modifyPortReq.InstanceId = &instanceId + if *modifyBandwidthReq.Bandwidth != 0 { + spinner.Start(fmt.Sprintf("Starting modify the pathx[%s] bandwidth", instanceId)) + if *modifyBandwidthReq.Bandwidth < 1 || *modifyBandwidthReq.Bandwidth > 100 { + spinner.Fail(fmt.Errorf("The value of bandwidth size cannot be less than 1 and cannot be greater than 100")) + return + } + modifyBandwidthReq.SetProjectIdRef(modifyInstanceReq.GetProjectIdRef()) + modifyBandwidthReq.SetRegionRef(modifyInstanceReq.GetRegionRef()) + modifyBandwidthReq.SetZoneRef(modifyInstanceReq.GetZoneRef()) + _, err := base.BizClient.ModifyUGA3Bandwidth(modifyBandwidthReq) + if err != nil { + spinner.Fail(err) + return + } + spinner.Stop() + } + if *modifyOriginInfoReq.OriginIPList != "" || *modifyOriginInfoReq.OriginDomain != "" { + spinner.Start(fmt.Sprintf("Starting modify the pathx[%s] origin information", instanceId)) + modifyOriginInfoReq.SetProjectIdRef(modifyInstanceReq.GetProjectIdRef()) + modifyOriginInfoReq.SetRegionRef(modifyInstanceReq.GetRegionRef()) + modifyOriginInfoReq.SetZoneRef(modifyInstanceReq.GetZoneRef()) + _, err := base.BizClient.ModifyUGA3OriginInfo(modifyOriginInfoReq) + if err != nil { + spinner.Fail(err) + return + } + spinner.Stop() + } + if *modifyInstanceReq.Name != "" || *modifyInstanceReq.Remark != "" { + spinner.Start(fmt.Sprintf("Starting modify the pathx[%s] resource information", instanceId)) + _, err := base.BizClient.ModifyUGA3Instance(modifyInstanceReq) + if err != nil { + spinner.Fail(err) + return + } + spinner.Stop() + } + + // modify port + tcpPortIntList := make([]int, 0) + rsTcpPortIntList := make([]int, 0) + if len(tcpPorts) > 0 || len(rsTcpPorts) > 0 { + spinner.Start(fmt.Sprintf("Starting modify the pathx[%s] port", instanceId)) + if len(tcpPorts) == 0 { + spinner.Fail(fmt.Errorf("The port cannot be empty.")) + return + } else if len(rsTcpPorts) == 0 { + spinner.Fail(fmt.Errorf("The origin-port cannot be empty.")) + return + } + if strings.EqualFold(protocol, "UDP") { + spinner.Fail(fmt.Errorf("The udp protocol is temporarily not supported for create")) + return + } else if !strings.EqualFold(protocol, "TCP") && + !strings.EqualFold(protocol, "UDP") { + spinner.Fail(fmt.Errorf("The value of protocol input error,please input 'TCP' or 'UDP',and the value entered is not case sensitive")) + return + } + tcpPortList, err := formatPortList(tcpPorts) + if err != nil { + spinner.Fail(err) + return + } + // tcpPorts convert to []int + for _, tcpPort := range tcpPortList { + port, _ := strconv.Atoi(tcpPort) + tcpPortIntList = append(tcpPortIntList, port) + } + rsTcpPortList, err := formatPortList(rsTcpPorts) + if err != nil { + spinner.Fail(err) + return + } + // rsTcpPorts convert to []int + for _, rsTcpPort := range rsTcpPortList { + rsPort, _ := strconv.Atoi(rsTcpPort) + rsTcpPortIntList = append(rsTcpPortIntList, rsPort) + } + if len(tcpPortIntList) != len(rsTcpPortIntList) { + spinner.Fail(fmt.Errorf("The number of port must be consistent with the number of origin-port.")) + return + } else if len(tcpPortIntList) >= 10 { + spinner.Fail(fmt.Errorf("The number of port cannot greater than or equals to 10")) + return + } + } + // ModifyUGA3Port + if len(tcpPortIntList) > 0 && len(rsTcpPortIntList) > 0 { + if strings.EqualFold(protocol, "TCP") { + modifyPortReq.TCP = tcpPortIntList + modifyPortReq.TCPRS = rsTcpPortIntList + } + modifyPortReq.SetProjectIdRef(modifyInstanceReq.GetProjectIdRef()) + modifyPortReq.SetRegionRef(modifyInstanceReq.GetRegionRef()) + modifyPortReq.SetZoneRef(modifyInstanceReq.GetZoneRef()) + _, err := base.BizClient.ModifyUGA3Port(modifyPortReq) + if err != nil { + base.HandleError(err) + return + } + spinner.Stop() + } + }, + } + flags := modifyCmd.Flags() + flags.SortFlags = false + + bindProjectID(modifyInstanceReq, flags) + bindRegion(modifyInstanceReq, flags) + bindZone(modifyInstanceReq, flags) + + flags.StringVar(&instanceId, "id", "", + "Required. It is the resource ID of the pathx") + modifyBandwidthReq.Bandwidth = flags.Int("bandwidth", 0, + "Optional. The bandwidth size. Its value range [1-100],no update if no value is specified") + modifyOriginInfoReq.OriginIPList = flags.String("origin-ip", "", + "Optional. Acceleration source IP. If multiple values exist,please split by ','") + modifyOriginInfoReq.OriginDomain = flags.String("origin-domain", "", + "Optional. Acceleration source domain name. Only 1 domain is supported") + modifyInstanceReq.Name = flags.String("name", "", + "Optional. Accelerate configuration resource name. If its value is not filled in or an empty string is not updated") + modifyInstanceReq.Remark = flags.String("remark", "", + "Optional. It will be modified if its value is not empty") + + flags.StringSliceVar(&tcpPorts, "port", nil, + "Optional. Disable 65123 port,the port can be multiple,please split by ',' for example 80,3000-3010. "+ + "The number of port must be consistent with the number of origin-port,and the number cannot greater than or equals to 10") + flags.StringSliceVar(&rsTcpPorts, "origin-port", nil, + "Optional. The origin-port can be multiple,please split by ',' for example 80,3000-3010."+ + "The number of origin-port must be consistent with the number of port") + flags.StringVar(&protocol, "protocol", "TCP", "Its values can be TCP and UDP, but currently only supports TCP") + + modifyCmd.MarkFlagRequired("id") + flags.SetFlagValuesFunc("id", func() []string { + return getPathxList(*modifyInstanceReq.ProjectId, *modifyInstanceReq.Region, *modifyInstanceReq.Zone) + }) + return modifyCmd +} + +// ucloud pathx list +func NewCmdUGA3List(out io.Writer) *cobra.Command { + getPathxListReq := base.BizClient.NewDescribeUGA3InstanceRequest() + var instanceId string + var detail bool + listCmd := &cobra.Command{ + Use: "list", + Short: "List all the pathx resource of project", + Long: "List all the pathx resource of project", + Example: "'ucloud pathx list or ucloud pathx list --id uga-xxx or ucloud pathx list --id uga-xxx --detail", + Run: func(cmd *cobra.Command, args []string) { + if len(instanceId) > 0 { + getPathxListReq.InstanceId = &instanceId + } + resp, err := base.BizClient.DescribeUGA3Instance(getPathxListReq) + if err != nil { + base.HandleError(err) + return + } + forwardInfos := resp.ForwardInstanceInfos + // may be no UGA3 instance under the current project + if len(forwardInfos) == 0 { + base.HandleError(fmt.Errorf("No pathx resource found under the current project.")) + return + } + // print pathx detail information + if detail && len(instanceId) > 0 { + instanceInfo := forwardInfos[0] + printPathxDetail(instanceInfo, out) + return + } + list := make([]Uga3DescribeRow, 0) + for _, item := range forwardInfos { + row := Uga3DescribeRow{} + row.ResourceID = item.InstanceId + row.CName = item.CName + row.Name = item.Name + row.AccelerationArea = item.AccelerationArea + row.Bandwidth = item.Bandwidth + row.OriginAreaCode = item.OriginAreaCode + row.IPList = strings.Join(item.IPList, ",") + row.Domain = item.Domain + row.CreateTime = base.FormatDate(item.CreateTime) + + var egressIps []string + for _, egressIp := range item.EgressIpList { + egressIps = append(egressIps, fmt.Sprintf("%s:%s", egressIp.Area, egressIp.IP)) + } + row.EgressIpList = strings.Join(egressIps, "|") + + list = append(list, row) + } + base.PrintTable(list, []string{ + "ResourceID", "CName", "Name", "AccelerationArea", "OriginAreaCode", + "Bandwidth", "EgressIpList", "IPList", "Domain", "CreateTime"}) + }, + } + flags := listCmd.Flags() + flags.SortFlags = false + + bindProjectID(getPathxListReq, flags) + bindRegion(getPathxListReq, flags) + bindZone(getPathxListReq, flags) + + flags.StringVar(&instanceId, "id", "", "Required. It is the resource ID of pathx resource") + flags.BoolVar(&detail, "detail", false, "Optional. If it is specified,the details will be printed") + flags.SetFlagValuesFunc("id", func() []string { + return getPathxList(*getPathxListReq.ProjectId, *getPathxListReq.Region, *getPathxListReq.Zone) + }) + return listCmd +} + +func printPathxDetail(instanceInfo pathx.ForwardInfo, out io.Writer) { + attrs := []base.DescribeTableRow{ + {Attribute: "ResourceID", Content: instanceInfo.InstanceId}, + {Attribute: "CName", Content: instanceInfo.CName}, + {Attribute: "Name", Content: instanceInfo.Name}, + {Attribute: "AccelerationArea", Content: instanceInfo.AccelerationArea}, + {Attribute: "AccelerationAreaName", Content: instanceInfo.AccelerationAreaName}, + {Attribute: "OriginAreaCode", Content: instanceInfo.OriginAreaCode}, + {Attribute: "OriginArea", Content: instanceInfo.OriginArea}, + {Attribute: "Bandwidth", Content: strconv.Itoa(instanceInfo.Bandwidth)}, + {Attribute: "ChargeType", Content: instanceInfo.ChargeType}, + {Attribute: "IPList", Content: strings.Join(instanceInfo.IPList, ",")}, + {Attribute: "Domain", Content: instanceInfo.Domain}, + {Attribute: "Remark", Content: instanceInfo.Remark}, + {Attribute: "CreateTime", Content: base.FormatDateTime(instanceInfo.CreateTime)}, + {Attribute: "ExpireTime", Content: base.FormatDateTime(instanceInfo.ExpireTime)}, + } + for _, attr := range attrs { + fmt.Fprintf(out, "%-22s: %s", attr.Attribute, attr.Content) + fmt.Println() + } + // 加速节点列表 + if len(instanceInfo.AccelerationAreaInfos) > 0 { + fmt.Println() + fmt.Fprintln(out, "Acceleration area list:") + for _, area := range instanceInfo.AccelerationAreaInfos { + fmt.Fprintf(out, "%s:%5s\n", "Area", area.AccelerationArea) + areaList := make([]PathxOptionalAreaRow, 0) + for _, node := range area.AccelerationNodes { + row := PathxOptionalAreaRow{ + AreaCode: node.AreaCode, + Area: node.Area, + FlagUnicode: node.FlagUnicode, + FlagEmoji: node.FlagEmoji, + } + areaList = append(areaList, row) + } + base.PrintTable(areaList, []string{"AreaCode", "Area", "FlagUnicode", "FlagEmoji"}) + } + } + // 回源出口IP地址 + if len(instanceInfo.EgressIpList) > 0 { + fmt.Println() + fmt.Fprintln(out, "Egress ip list:") + egressIpList := make([]EgressIpInfoRow, 0) + for _, egressIp := range instanceInfo.EgressIpList { + row := EgressIpInfoRow{ + IP: egressIp.IP, + Area: egressIp.Area, + } + egressIpList = append(egressIpList, row) + } + base.PrintTable(egressIpList, []string{"Area", "IP"}) + } + if len(instanceInfo.PortSets) > 0 { + fmt.Println() + fmt.Fprintln(out, "Port list:") + portList := make([]Uga3PortRow, 0) + for _, portItem := range instanceInfo.PortSets { + row := Uga3PortRow{ + Protocol: portItem.Protocol, + Port: portItem.Port, + RSPort: portItem.RSPort, + } + portList = append(portList, row) + } + base.PrintTable(portList, []string{"Protocol", "Port", "RSPort"}) + } +} + +// ucloud pathx-price +func NewCmdPathxPrice(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "price", + Short: "List all the acceleration area price", + Long: "List all the acceleration area price", + } + cmd.AddCommand(NewPathxPriceList(out)) + // temporary not supports + //cmd.AddCommand(NewPathxPriceUpgradeInfo()) + return cmd +} + +// ucloud pathx price list +func NewPathxPriceList(out io.Writer) *cobra.Command { + priceReq := base.BizClient.NewGetUGA3PriceRequest() + cmd := &cobra.Command{ + Use: "list", + Short: "List all the pathx acceleration area price", + Long: "List all the pathx acceleration area price", + Example: "ucloud pathx price list --bandwidth 10 --area-code BKK --charge-type Month", + Run: func(cmd *cobra.Command, args []string) { + if strings.EqualFold(*priceReq.ChargeType, "Month") { + *priceReq.Quantity = 0 + } else if *priceReq.Quantity <= 0 { + base.HandleError(fmt.Errorf("If the value of charge-type is 'Year' or 'Hour',its value must be greater than 0")) + return + } + + switch strings.ToLower(*priceReq.ChargeType) { + case "hour": + *priceReq.ChargeType = "Dynamic" + case "month": + *priceReq.ChargeType = "Month" + case "year": + *priceReq.ChargeType = "Year" + } + + response, err := base.BizClient.GetUGA3Price(priceReq) + if err != nil { + base.HandleError(err) + return + } + list := make([]UGA3PriceRow, 0) + priceList := response.UGA3Price + if len(priceList) == 0 { + base.HandleError(fmt.Errorf("Not found acceleration area price information.")) + return + } + //fmt.Fprintf(out,"Aceeleration area price information (unit:¥) :") + for _, info := range priceList { + row := UGA3PriceRow{ + AccelerationBandwidthPrice: fmt.Sprintf("%s%s", "¥", strconv.FormatFloat(info.AccelerationBandwidthPrice, 'g', 12, 64)), + //AccelerationAreaName: info.AccelerationAreaName, + AccelerationForwarderPrice: fmt.Sprintf("%s%s", "¥", strconv.FormatFloat(info.AccelerationForwarderPrice, 'g', 12, 64)), + AccelerationArea: info.AccelerationArea, + } + list = append(list, row) + } + base.PrintTable(list, []string{"AccelerationArea", "AccelerationBandwidthPrice", "AccelerationForwarderPrice"}) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + bindProjectID(priceReq, flags) + bindRegion(priceReq, flags) + bindZone(priceReq, flags) + + priceReq.Bandwidth = flags.Int("bandwidth", 1, + "Required. The bandwidth of acceleration area to get price") + priceReq.AreaCode = flags.String("area-code", "", + "Required. The area-code of acceleration area to get price") + priceReq.Quantity = flags.Int("quantity", 1, + "Optional. When the value of the charge-type is 'Month',its default value is 0,"+ + "if the value of charge-type is 'Year' or 'Hour',its value must be greater than 0") + priceReq.ChargeType = flags.String("charge-type", "", + "Optional. Its value is not case sensitive,acceptable values:'Year',pay yearly;'Month',pay monthly;'Hour',pay hourly") + priceReq.AccelerationArea = flags.String("accel", "", + "Optional. The acceleration-area to get price") + + _ = cmd.MarkFlagRequired("bandwidth") + _ = cmd.MarkFlagRequired("area-code") + _ = flags.SetFlagValues("area-code", "BKK", "DXB", "FRA", "SGN", "HKG", "CGK", "LOS", "LHR", "LAX", "MNL", "DME", "BOM", "MSP", "ICN", "PVG", "SIN", "NRT", "IAD", "TPE") + _ = flags.SetFlagValues("charge-type", "Year", "Month", "Hour") + _ = flags.SetFlagValues("accel", "Global", "AP", "EU", "ME", "OA", "AF", "NA", "SA") + return cmd +} + +// ucloud pathx-area +func NewCmdPathxArea(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "area", + Short: "List origin area or acceleration area information", + Long: "List origin area or acceleration area information", + } + cmd.AddCommand(NewCmdPathxAreaList(out)) + return cmd +} + +// ucloud pathx area list +func NewCmdPathxAreaList(out io.Writer) *cobra.Command { + areaGetReq := base.BizClient.NewDescribeUGA3AreaRequest() + optimizationReq := base.BizClient.NewDescribeUGA3OptimizationRequest() + var timeRange, accelerationArea, originDomain, originIp string + var noAccel bool + cmd := &cobra.Command{ + Use: "list", + Short: "List origin area or acceleration area information", + Long: "Provide optional flags to get the optional list of global access source stations", + Example: "ucloud pathx area list --origin-ip 0.0.0.0 --origin-domain test.com", + Run: func(cmd *cobra.Command, args []string) { + if len(originDomain) == 0 && len(originIp) == 0 { + response, err := base.BizClient.DescribeUGA3Area(areaGetReq) + if err != nil { + base.HandleError(err) + return + } + forwardAreas := response.AreaSet + if len(forwardAreas) == 0 { + base.HandleError(fmt.Errorf("Not found the origin area list")) + return + } + areasGroup := make(map[string][]PathxOptionalAreaRow) + for _, item := range forwardAreas { + if areasGroup[item.ContinentCode] == nil { + areasGroup[item.ContinentCode] = make([]PathxOptionalAreaRow, 0) + } + areasGroup[item.ContinentCode] = append(areasGroup[item.ContinentCode], PathxOptionalAreaRow{ + AreaCode: item.AreaCode, + Area: item.Area, + CountryCode: item.CountryCode, + FlagUnicode: item.FlagUnicode, + FlagEmoji: item.FlagEmoji, + }) + } + fmt.Fprintln(out, "Origin areas :") + for area := range areasGroup { + fmt.Fprintf(out, "ContinentCode: %s\n", area) + rows := areasGroup[area] + base.PrintTable(rows, []string{"AreaCode", "Area", "CountryCode", "FlagUnicode", "FlagEmoji"}) + fmt.Println() + } + return + } + areaGetReq.Domain = &originDomain + areaGetReq.IPList = &originIp + response, err := base.BizClient.DescribeUGA3Area(areaGetReq) + if err != nil { + base.HandleError(err) + return + } + forwardAreas := response.AreaSet + if len(forwardAreas) == 0 { + base.HandleError(fmt.Errorf("Not found the origin area list")) + return + } + // recommend one area for user + forwardArea := forwardAreas[0] + + fmt.Fprintf(out, "Recommend origin area:(%s)\n", forwardArea.ContinentCode) + areas := make([]PathxOptionalAreaRow, 0) + areas = append(areas, PathxOptionalAreaRow{ + AreaCode: forwardArea.AreaCode, + Area: forwardArea.Area, + CountryCode: forwardArea.CountryCode, + FlagUnicode: forwardArea.FlagUnicode, + FlagEmoji: forwardArea.FlagEmoji, + }) + base.PrintTable(areas, []string{"AreaCode", "Area", "CountryCode", "FlagUnicode", "FlagEmoji"}) + fmt.Println() + + // display acceleration areas + if !noAccel { + areaCode := forwardAreas[0].AreaCode + optimizationReq.AreaCode = &areaCode + optimizationReq.AccelerationArea = &accelerationArea + optimizationReq.TimeRange = &timeRange + optimizationReq.SetProjectIdRef(areaGetReq.GetProjectIdRef()) + optimizationReq.SetRegionRef(areaGetReq.GetRegionRef()) + optimizationReq.SetZoneRef(areaGetReq.GetZoneRef()) + optimizationResponse, err := base.BizClient.DescribeUGA3Optimization(optimizationReq) + if err != nil { + base.HandleError(err) + return + } + accelerationInfos := optimizationResponse.AccelerationInfos + if len(accelerationInfos) == 0 { + base.HandleError(fmt.Errorf("Not found the acceleration area information.")) + return + } + fmt.Fprintf(out, "Acceleration areas :\n") + for _, item := range accelerationInfos { + // User did not provide acceleration-area flag + if len(accelerationArea) == 0 { + fmt.Fprintf(out, "%s(%s):\n", item.AccelerationName, item.AccelerationArea) + } + list := make([]PathxOptimizationRow, 0) + nodeDelays := item.NodeInfo + for _, node := range nodeDelays { + row := PathxOptimizationRow{} + row.Area = node.Area + row.AreaCode = node.AreaCode + row.CountryCode = node.CountryCode + row.FlagUnicode = node.FlagUnicode + row.FlagEmoji = node.FlagEmoji + row.Latency = fmt.Sprintf("%s%s", strconv.FormatFloat(node.Latency, 'g', 12, 64), "ms") + row.LatencyWAN = fmt.Sprintf("%s%s", strconv.FormatFloat(node.LatencyInternet, 'g', 12, 64), "ms") + row.LatencyPathX = fmt.Sprintf("%s%s", strconv.FormatFloat(node.LatencyOptimization, 'g', 12, 64), "%") + row.Loss = fmt.Sprintf("%s%s", strconv.FormatFloat(node.Loss, 'g', 12, 64), "%") + row.LossWAN = fmt.Sprintf("%s%s", strconv.FormatFloat(node.LossInternet, 'g', 12, 64), "%") + row.LossPathx = fmt.Sprintf("%s%s", strconv.FormatFloat(node.LossOptimization, 'g', 12, 64), "%") + list = append(list, row) + } + base.PrintTable(list, []string{"AreaCode", "Area", "CountryCode", "FlagUnicode", "FlagEmoji", + "Latency", "LatencyWAN", "LatencyPathX", "Loss", "LossWAN", "LossPathx"}) + } + return + } + + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + bindProjectID(areaGetReq, flags) + bindRegion(areaGetReq, flags) + bindZone(areaGetReq, flags) + + flags.StringVar(&timeRange, "time-range", "", + "Optional. The default value is 1 day. Acceptable values:'Hour','Day','Week',and its value is not case sensitive") + flags.StringVar(&accelerationArea, "accel", "", + "Optional. The acceleration area,acceptable values:'Global','AP','EU','ME','OA','AF','NA','SA'") + flags.StringVar(&originDomain, "origin-domain", "", + "Optional. If you fill in the IP or domain name, a region will be recommended as the first in the return list") + flags.StringVar(&originIp, "origin-ip", "", + "Optional. If you fill in the IP or domain name, a region will be recommended as the first IP collection of the source station in the return list, "+ + "split by ',' example:110.10.10.1,111.100.0.10 ") + flags.BoolVar(&noAccel, "no-accel", false, + "Optional. If it is specified,the print result will not be displayed acceleration areas") + + return cmd +} + +type UGA3PriceRow struct { + // 加速大区代码 + AccelerationArea string + // 加速大区名称 + AccelerationAreaName string + // 转发配置价格 + AccelerationForwarderPrice string + // 加速配置带宽价格 + AccelerationBandwidthPrice string +} + +// describe UGA3 instance information row +type Uga3DescribeRow struct { + // 加速配置ID + ResourceID string + // 加速域名 + CName string + // 加速实例名称 + Name string + // 加速区域 + AccelerationArea string + // 加速区域名称 + AccelerationAreaName string + // 回源出口IP地址 + EgressIpList string + // 购买的带宽值 + Bandwidth int + // 备注 + Remark string + // 源站中文名 + OriginArea string + // 源站AreaCode + OriginAreaCode string + // 资源创建时间 + CreateTime string + // 资源过期时间 + ExpireTime string + // 计费方式 + ChargeType string + // 源站IP列表,多个值由半角英文逗号相隔 + IPList string + // 源站域名 + Domain string +} + +// pathx port print row +type Uga3PortRow struct { + // 转发协议,枚举值["TCP","UDP","HTTPHTTP","HTTPSHTTP","HTTPSHTTPS","WSWS","WSSWS","WSSWSS"]。TCP和UDP代表四层转发,其余为七层转发。 + Protocol string + // 源站服务器监听的端口号 + RSPort int + // 加速端口 + Port int +} + +// pathx price upgrade-info print row +type PathxUpdatePriceRow struct { + // 实例ID + InstanceId string + // 带宽 + Bandwidth int + // 更新价格 + UpdatePrice float64 +} + +// pathx optimization print row +type PathxOptimizationRow struct { + // 加速大区名称 + AccelerationName string + // 加速大区代码 + AccelerationArea string + // 加速区域 + Area string + // 加速区域Code + AreaCode string + // 国家代码 + CountryCode string + // 国旗Code + FlagUnicode string + // 国旗Emoji + FlagEmoji string + // 加速延迟 + Latency string + // 公网延迟 + LatencyWAN string + // 加速提升比例 + LatencyPathX string + // 加速后丢包率 + Loss string + // 原始丢包率 + LossWAN string + // 丢包下降比例 + LossPathx string +} + +// row for print of pathx area +type PathxOptionalAreaRow struct { + AreaCode string + Area string + CountryCode string + FlagUnicode string + FlagEmoji string + ContinentCode string +} + +// row for print of egressIpList +type EgressIpInfoRow struct { + // 线路出口EIP + IP string + // 线路出口机房代号 + Area string +} + +// NewCmdUpath ucloud pathx upath +func NewCmdUpath() *cobra.Command { + cmd := &cobra.Command{ + Use: "upath", + Short: "List pathx upath instances", + Long: "List pathx upath instances", + } + out := base.Cxt.GetWriter() + cmd.AddCommand(NewCmdUpathList(out)) + return cmd +} + +type upathRow struct { + ResourceID string + UPathName string + AcceleratedPath string + BoundUGA string +} + +// NewCmdUpathList ucloud pathx upath list +func NewCmdUpathList(out io.Writer) *cobra.Command { + req := base.BizClient.PrivatePathxClient.NewDescribeUPathRequest() + cmd := &cobra.Command{ + Use: "list", + Short: "list upath instances", + Long: "list upath instances", + Run: func(c *cobra.Command, args []string) { + resp, err := base.BizClient.PrivatePathxClient.DescribeUPath(req) + if err != nil { + base.HandleError(err) + return + } + list := make([]upathRow, 0) + for _, ins := range resp.UPathSet { + row := upathRow{ + ResourceID: ins.UPathId, + UPathName: ins.Name, + AcceleratedPath: fmt.Sprintf("%s->%s %dM", ins.LineFromName, ins.LineToName, ins.Bandwidth), + } + ids := []string{} + for _, ga := range ins.UGAList { + ids = append(ids, ga.UGAId) + } + row.BoundUGA = strings.Join(ids, ",") + list = append(list, row) + } + base.PrintList(list, out) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + bindProjectID(req, flags) + req.UPathId = flags.String("upath-id", "", "Optional. Resource ID of upath instance to list") + + return cmd +} + +// NewCmdUGA ucloud uga +func NewCmdUGA() *cobra.Command { + cmd := &cobra.Command{ + Use: "uga", + Short: "Create,list,update and delete pathx uga instances", + Long: `Create,list,update and delete pathx uga instances`, + } + + out := base.Cxt.GetWriter() + cmd.AddCommand(NewCmdUGAList(out)) + cmd.AddCommand(NewCmdUGADescribe(out)) + cmd.AddCommand(NewCmdUGACreate(out)) + cmd.AddCommand(NewCmdUGADelete(out)) + cmd.AddCommand(NewCmdUGAAddPort(out)) + cmd.AddCommand(NewCmdUGARemovePort(out)) + + return cmd +} + +// UGARow 表格行 +type UGARow struct { + ResourceID string + UGAName string + CName string + Origin string + AcceleratedPath string +} + +var protocols = []string{"tcp", "udp"} + +// NewCmdUGAList ucloud uga list +func NewCmdUGAList(out io.Writer) *cobra.Command { + req := base.BizClient.PrivatePathxClient.NewDescribeUGAInstanceRequest() + cmd := &cobra.Command{ + Use: "list", + Short: "list uga instances", + Long: "list uga instances", + Run: func(c *cobra.Command, args []string) { + *req.UGAId = base.PickResourceID(*req.UGAId) + resp, err := base.BizClient.PrivatePathxClient.DescribeUGAInstance(req) + if err != nil { + base.HandleError(err) + return + } + + list := make([]UGARow, 0) + for _, ins := range resp.UGAList { + row := UGARow{ + ResourceID: ins.UGAId, + UGAName: ins.UGAName, + CName: ins.CName, + Origin: fmt.Sprintf("%s%s", strings.Join(ins.IPList, ","), ins.Domain), + } + row.AcceleratedPath = getUpathStr(ins.UPathSet) + list = append(list, row) + } + base.PrintList(list, out) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + req.UGAId = flags.String("uga-id", "", "Optional. Resource ID of uga instance") + bindProjectID(req, flags) + + return cmd +} + +func getUpathStr(list []ppathx.UPathSet) string { + paths := make([]string, 0) + for _, p := range list { + paths = append(paths, fmt.Sprintf("%s->%s %dM", p.LineFromName, p.LineToName, p.Bandwidth)) + } + return strings.Join(paths, "\n") +} + +func getOutIPStr(list []ppathx.OutPublicIpInfo) string { + strs := make([]string, 0) + for _, p := range list { + strs = append(strs, fmt.Sprintf("%s %s", p.IP, base.RegionLabel[p.Area])) + } + return strings.Join(strs, "\n") +} + +func getPortStr(list []ppathx.UGAATask) string { + strs := make([]string, 0) + for _, t := range list { + strs = append(strs, fmt.Sprintf("%s %d", t.Protocol, t.Port)) + } + return strings.Join(strs, "\n") +} + +// NewCmdUGADescribe ucloud uga describe +func NewCmdUGADescribe(out io.Writer) *cobra.Command { + req := base.BizClient.PrivatePathxClient.NewDescribeUGAInstanceRequest() + cmd := &cobra.Command{ + Use: "describe", + Short: "Display detail informations about uga instances", + Long: "Display detail informations about uga instances", + Run: func(c *cobra.Command, args []string) { + *req.UGAId = base.PickResourceID(*req.UGAId) + resp, err := base.BizClient.PrivatePathxClient.DescribeUGAInstance(req) + if err != nil { + base.HandleError(err) + return + } + if len(resp.UGAList) != 1 { + base.HandleError(fmt.Errorf("uga[%s] may not exist", *req.UGAId)) + return + } + + ins := resp.UGAList[0] + list := []base.DescribeTableRow{ + {"ResourceID", ins.UGAId}, + {"UGAName", ins.UGAName}, + {"Origin", fmt.Sprintf("%s%s", ins.Domain, strings.Join(ins.IPList, ","))}, + {"CName", ins.CName}, + {"AcceleratedPath", getUpathStr(ins.UPathSet)}, + {"OutIP", getOutIPStr(ins.OutPublicIpList)}, + {"Port", getPortStr(ins.TaskSet)}, + } + base.PrintList(list, out) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + req.UGAId = flags.String("uga-id", "", "Required. Resource ID of uga instance") + bindProjectID(req, flags) + + cmd.MarkFlagRequired("uga-id") + flags.SetFlagValuesFunc("uga-id", func() []string { + return getUGAIDList(*req.ProjectId) + }) + + return cmd +} + +func formatPortList(userPorts []string) ([]string, error) { + portList := make([]string, 0) + for _, port := range userPorts { + if strings.Contains(port, "-") { + portRange := strings.Split(port, "-") + if len(portRange) != 2 { + return nil, fmt.Errorf("port %s is invalid, it's pattern should be like 3000-3100", port) + } + min, err := strconv.Atoi(portRange[0]) + if err != nil { + return nil, fmt.Errorf("parse port failed: %v", err) + } + max, err := strconv.Atoi(portRange[1]) + if err != nil { + return nil, fmt.Errorf("parse port failed: %v", err) + } + + for i := min; i <= max; i++ { + portList = append(portList, strconv.Itoa(i)) + } + } else { + portList = append(portList, port) + } + } + return portList, nil +} + +// NewCmdUGACreate ucloud uga create +func NewCmdUGACreate(out io.Writer) *cobra.Command { + var protocol string + var ports, lines []string + req := base.BizClient.PrivatePathxClient.NewCreateUGAInstanceRequest() + cmd := &cobra.Command{ + Use: "create", + Short: "Create uga instance", + Long: "Create uga instance", + Example: "ucloud pathx uga create --name testcli1 --protocol tcp --origin-location 中国 --origin-domain lixiaojun.xyz --upath-id upath-auvfexxx/test_0 --port 80-90,100,110-115", + Run: func(c *cobra.Command, args []string) { + if *req.IPList == "" && *req.Domain == "" { + fmt.Fprintln(out, "origin-ip and origin-domain can not be both empty") + return + } + + portList, err := formatPortList(ports) + if err != nil { + base.HandleError(err) + return + } + + switch strings.ToLower(protocol) { + case "tcp": + req.TCP = portList + case "udp": + req.UDP = portList + case "http": + req.HTTP = portList + case "https": + req.HTTPS = portList + default: + fmt.Fprintf(out, "protocol should be one of %s, received:%s\n", strings.Join(protocols, ","), protocol) + } + + resp, err := base.BizClient.PrivatePathxClient.CreateUGAInstance(req) + if err != nil { + if uErr, ok := err.(uerr.Error); ok && uErr.Code() == 33756 { + fmt.Fprintf(out, "The number of ports added exceeds the limit(50). We recommend that you could reduce the number of ports, then create an uga instance, \nand then add the remaining ports by executing 'ucloud pathx uga add-port --protocol %s --uga-id --port '\n", protocol) + } + return + } + + fmt.Fprintf(out, "uga[%s] created\n", resp.UGAId) + + for _, path := range lines { + p := base.PickResourceID(path) + bindReq := base.BizClient.PrivatePathxClient.NewUGABindUPathRequest() + bindReq.ProjectId = req.ProjectId + bindReq.UGAId = sdk.String(resp.UGAId) + bindReq.UPathId = &p + _, err := base.BizClient.PrivatePathxClient.UGABindUPath(bindReq) + if err != nil { + fmt.Fprintf(out, "bind uga[%s] and upath[%s] failed: %v\n", resp.UGAId, p, err) + } else { + fmt.Fprintf(out, "bound uga[%s] and upath[%s]\n", resp.UGAId, p) + } + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + bindProjectID(req, flags) + req.Name = flags.String("name", "", "Required. Name of uga instance to create") + req.IPList = flags.String("origin-ip", "", "Required if origin-domain is empty. IP address of origin. multiple IP address separated by ','") + req.Domain = flags.String("origin-domain", "", "Required if origin-ip is empty.") + req.Location = flags.String("origin-location", "", "Required. Location of origin ip or domain. accpet valeus:'中国','洛杉矶','法兰克福','中国香港','雅加达','孟买','东京','莫斯科','新加坡','曼谷','中国台北','华盛顿','首尔'") + flags.StringVar(&protocol, "protocol", "", fmt.Sprintf("Required. accept values: %s", strings.Join(protocols, ","))) + flags.StringSliceVar(&ports, "port", nil, "Required. Single port or port range, separated by ',', for example 80,3000-3010") + flags.StringSliceVar(&lines, "upath-id", nil, "Required. Accelerated path to bind with the uga instance to create. multiple upath-id separated by ','; see 'ucloud pathx upath list") + + cmd.MarkFlagRequired("name") + cmd.MarkFlagRequired("origin-location") + cmd.MarkFlagRequired("protocol") + cmd.MarkFlagRequired("port") + cmd.MarkFlagRequired("upath-id") + + flags.SetFlagValues("origin-location", "中国", "洛杉矶", "法兰克福", "中国香港", "雅加达", "孟买", "东京", "莫斯科", "新加坡", "曼谷", "中国台北", "华盛顿", "首尔") + flags.SetFlagValues("protocol", protocols...) + flags.SetFlagValuesFunc("upath-id", func() []string { + return getUpathIDList(*req.ProjectId) + }) + + return cmd +} + +// NewCmdUGADelete ucloud uga delete +func NewCmdUGADelete(out io.Writer) *cobra.Command { + idNames := []string{} + req := base.BizClient.PrivatePathxClient.NewDeleteUGAInstanceRequest() + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete uga instances", + Long: "Delete uga instances", + Run: func(c *cobra.Command, args []string) { + for _, idname := range idNames { + id := base.PickResourceID(idname) + req.UGAId = &id + _, err := base.BizClient.PrivatePathxClient.DeleteUGAInstance(req) + if err != nil { + base.HandleError(err) + } else { + fmt.Fprintf(out, "uga[%s] deleted\n", id) + } + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + bindProjectID(req, flags) + flags.StringSliceVar(&idNames, "uga-id", nil, "Required. Resource ID of uga instances to delete. Multiple resource ids separated by comma") + + cmd.MarkFlagRequired("uga-id") + flags.SetFlagValuesFunc("uga-id", func() []string { + return getUGAIDList(*req.ProjectId) + }) + + return cmd +} + +// NewCmdUGAAddPort ucloud pathx uga add-port +func NewCmdUGAAddPort(out io.Writer) *cobra.Command { + var ports []string + var protocol string + req := base.BizClient.NewAddUGATaskRequest() + cmd := &cobra.Command{ + Use: "add-port", + Short: "Add port for uga instance", + Long: "Add port for uga instance", + Run: func(c *cobra.Command, args []string) { + portList, err := formatPortList(ports) + if err != nil { + base.HandleError(err) + return + } + + switch strings.ToLower(protocol) { + case "tcp": + req.TCP = portList + case "udp": + req.UDP = portList + case "http": + req.HTTP = portList + case "https": + req.HTTPS = portList + default: + fmt.Fprintf(out, "protocol should be one of %s, received:%s\n", strings.Join(protocols, ","), protocol) + } + + *req.UGAId = base.PickResourceID(*req.UGAId) + _, err = base.BizClient.AddUGATask(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintf(out, "port %v added\n", ports) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + bindProjectID(req, flags) + req.UGAId = flags.String("uga-id", "", "Required. Resource ID of uga instance to add port") + flags.StringVar(&protocol, "protocol", "", fmt.Sprintf("Required. accept values: %s", strings.Join(protocols, ","))) + flags.StringSliceVar(&ports, "port", nil, "Required. Single port or port range, separated by ',', for example 80,3000-3010") + + cmd.MarkFlagRequired("protocol") + cmd.MarkFlagRequired("uga-id") + cmd.MarkFlagRequired("port") + + flags.SetFlagValues("protocol", protocols...) + flags.SetFlagValuesFunc("uga-id", func() []string { + return getUGAIDList(*req.ProjectId) + }) + + return cmd +} + +// NewCmdUGARemovePort ucloud pathx uga delete-port +func NewCmdUGARemovePort(out io.Writer) *cobra.Command { + var ports []string + var protocol string + req := base.BizClient.NewDeleteUGATaskRequest() + cmd := &cobra.Command{ + Use: "delete-port", + Short: "Delete port for uga instance", + Long: "Delete port for uga instance", + Run: func(c *cobra.Command, args []string) { + portList, err := formatPortList(ports) + if err != nil { + base.HandleError(err) + return + } + + switch strings.ToLower(protocol) { + case "tcp": + req.TCP = portList + case "udp": + req.UDP = portList + case "http": + req.HTTP = portList + case "https": + req.HTTPS = portList + default: + fmt.Fprintf(out, "protocol should be one of %s, received:%s\n", strings.Join(protocols, ","), protocol) + } + + *req.UGAId = base.PickResourceID(*req.UGAId) + _, err = base.BizClient.DeleteUGATask(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintf(out, "port %v deleted\n", ports) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + bindProjectID(req, flags) + req.UGAId = flags.String("uga-id", "", "Required. Resource ID of uga instance to delete port") + flags.StringVar(&protocol, "protocol", "", fmt.Sprintf("Required. accept values: %s", strings.Join(protocols, ","))) + flags.StringSliceVar(&ports, "port", nil, "Required. Single port or port range, separated by ',', for example 80,3000-3010") + + cmd.MarkFlagRequired("protocol") + cmd.MarkFlagRequired("uga-id") + cmd.MarkFlagRequired("port") + + flags.SetFlagValues("protocol", protocols...) + flags.SetFlagValuesFunc("uga-id", func() []string { + return getUGAIDList(*req.ProjectId) + }) + + return cmd +} + +func getUGAList(project string) ([]ppathx.UGAAInfo, error) { + req := base.BizClient.PrivatePathxClient.NewDescribeUGAInstanceRequest() + req.ProjectId = &project + resp, err := base.BizClient.PrivatePathxClient.DescribeUGAInstance(req) + if err != nil { + return nil, err + } + return resp.UGAList, nil +} + +func getUGAIDList(project string) []string { + list, err := getUGAList(project) + if err != nil { + base.LogError(fmt.Sprintf("getUDGAIDList filed:%v", err)) + return nil + } + strs := make([]string, 0) + for _, ins := range list { + strs = append(strs, fmt.Sprintf("%s/%s", ins.UGAId, ins.UGAName)) + } + return strs +} + +func getUpathList(project string) ([]ppathx.UPathInfo, error) { + req := base.BizClient.PrivatePathxClient.NewDescribeUPathRequest() + req.ProjectId = &project + resp, err := base.BizClient.PrivatePathxClient.DescribeUPath(req) + if err != nil { + return nil, err + } + return resp.UPathSet, nil +} + +func getUpathIDList(project string) []string { + list, err := getUpathList(project) + if err != nil { + base.LogError(fmt.Sprintf("getUpathIDList failed:%v", err)) + return nil + } + strs := make([]string, 0) + for _, ins := range list { + strs = append(strs, fmt.Sprintf("%s/%s", ins.UPathId, ins.Name)) + } + return strs +} diff --git a/cmd/project.go b/cmd/project.go index 4a7648fa77..1b36152a81 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -15,14 +15,16 @@ package cmd import ( + "io" + "github.com/spf13/cobra" "github.com/ucloud/ucloud-sdk-go/services/uaccount" - . "github.com/ucloud/ucloud-cli/base" + "github.com/ucloud/ucloud-cli/base" ) -//NewCmdProject ucloud project +// NewCmdProject ucloud project func NewCmdProject() *cobra.Command { var cmd = &cobra.Command{ Use: "project", @@ -30,44 +32,45 @@ func NewCmdProject() *cobra.Command { Long: "List,create,update and delete project", Example: "ucloud project", } - cmd.AddCommand(NewCmdProjectList()) + out := base.Cxt.GetWriter() + cmd.AddCommand(NewCmdProjectList(out)) cmd.AddCommand(NewCmdProjectCreate()) cmd.AddCommand(NewCmdProjectUpdate()) cmd.AddCommand(NewCmdProjectDelete()) return cmd } -//NewCmdProjectList ucloud project list -func NewCmdProjectList() *cobra.Command { - var cmd = &cobra.Command{ +// NewCmdProjectList ucloud project list +func NewCmdProjectList(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ Use: "list", Short: "List project", Long: "List project", Example: "ucloud project list", Run: func(cmd *cobra.Command, args []string) { - listProject() + listProject(out) }, } return cmd } -//NewCmdProjectCreate ucloud project create +// NewCmdProjectCreate ucloud project create func NewCmdProjectCreate() *cobra.Command { - req := BizClient.NewCreateProjectRequest() + req := base.BizClient.NewCreateProjectRequest() cmd := &cobra.Command{ Use: "create", Short: "Create project", Long: "Create project", Example: "ucloud project create --name xxx", Run: func(cmd *cobra.Command, args []string) { - resp, err := BizClient.CreateProject(req) + resp, err := base.BizClient.CreateProject(req) if err != nil { - Cxt.PrintErr(err) + base.Cxt.PrintErr(err) } else { if resp.RetCode != 0 { - HandleBizError(resp) + base.HandleBizError(resp) } else { - Cxt.Printf("Project:%q created successfully.\n", resp.ProjectId) + base.Cxt.Printf("Project:%q created\n", resp.ProjectId) } } }, @@ -78,23 +81,23 @@ func NewCmdProjectCreate() *cobra.Command { return cmd } -//NewCmdProjectUpdate ucloud project update +// NewCmdProjectUpdate ucloud project update func NewCmdProjectUpdate() *cobra.Command { - req := BizClient.NewModifyProjectRequest() + req := base.BizClient.NewModifyProjectRequest() cmd := &cobra.Command{ Use: "update", Short: "Update project name", Long: "Update project name", Example: "ucloud project update --id org-xxx --name new_name", Run: func(cmd *cobra.Command, args []string) { - resp, err := BizClient.ModifyProject(req) + resp, err := base.BizClient.ModifyProject(req) if err != nil { - Cxt.PrintErr(err) + base.Cxt.PrintErr(err) } else { if resp.RetCode != 0 { - HandleBizError(resp) + base.HandleBizError(resp) } else { - Cxt.Printf("Project:%s updated successfully.\n", *req.ProjectId) + base.Cxt.Printf("Project:%s updated\n", *req.ProjectId) } } }, @@ -106,23 +109,23 @@ func NewCmdProjectUpdate() *cobra.Command { return cmd } -//NewCmdProjectDelete ucloud project delete +// NewCmdProjectDelete ucloud project delete func NewCmdProjectDelete() *cobra.Command { - req := BizClient.NewTerminateProjectRequest() + req := base.BizClient.NewTerminateProjectRequest() cmd := &cobra.Command{ Use: "delete", Short: "Delete project", Long: "Delete project", Example: "ucloud project delete --id org-xxx", Run: func(cmd *cobra.Command, args []string) { - resp, err := BizClient.TerminateProject(req) + resp, err := base.BizClient.TerminateProject(req) if err != nil { - Cxt.PrintErr(err) + base.Cxt.PrintErr(err) } else { if resp.RetCode != 0 { - HandleBizError(resp) + base.HandleBizError(resp) } else { - Cxt.Printf("Project:%s deleted successfully.\n", *req.ProjectId) + base.Cxt.Printf("Project:%s deleted\n", *req.ProjectId) } } }, @@ -132,19 +135,31 @@ func NewCmdProjectDelete() *cobra.Command { return cmd } -func listProject() error { +func listProject(out io.Writer) error { req := &uaccount.GetProjectListRequest{} - resp, err := BizClient.GetProjectList(req) + resp, err := base.BizClient.GetProjectList(req) if err != nil { return err } if resp.RetCode != 0 { - return HandleBizError(resp) + return base.HandleBizError(resp) } - if global.json { - PrintJSON(resp.ProjectSet) - } else { - PrintTable(resp.ProjectSet, []string{"ProjectId", "ProjectName"}) + if global.JSON { + return base.PrintJSON(resp.ProjectSet, out) } + base.PrintTable(resp.ProjectSet, []string{"ProjectId", "ProjectName"}) return nil } + +func getProjectList() []string { + req := &uaccount.GetProjectListRequest{} + resp, err := base.BizClient.GetProjectList(req) + if err != nil { + return nil + } + list := []string{} + for _, p := range resp.ProjectSet { + list = append(list, p.ProjectId+"/"+p.ProjectName) + } + return list +} diff --git a/cmd/region.go b/cmd/region.go index 1e66911fc2..17275532fb 100644 --- a/cmd/region.go +++ b/cmd/region.go @@ -16,7 +16,9 @@ package cmd import ( "encoding/json" + "errors" "fmt" + "io" "io/ioutil" "strings" @@ -24,37 +26,33 @@ import ( "github.com/ucloud/ucloud-sdk-go/services/uaccount" - . "github.com/ucloud/ucloud-cli/base" + "github.com/ucloud/ucloud-cli/base" ) -//NewCmdRegion ucloud region -func NewCmdRegion() *cobra.Command { - var cmd = &cobra.Command{ +// NewCmdRegion ucloud region +func NewCmdRegion(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ Use: "region", Short: "List all region and zone", Long: "List all region and zone", Example: "ucloud region", Run: func(cmd *cobra.Command, args []string) { - regionMap, err := fetchRegion() + regionIns, err := fetchRegion() if err != nil { - HandleError(err) + base.HandleError(err) return } regionList := make([]RegionTable, 0) - for region, zones := range regionMap { + for region, zones := range regionIns.Labels { regionList = append(regionList, RegionTable{region, strings.Join(zones, ", ")}) } - if global.json { - PrintJSON(regionList) - } else { - PrintTableS(regionList) - } + base.PrintList(regionList, out) }, } return cmd } -//RegionTable 为显示region表格创建的类型 +// RegionTable 为显示region表格创建的类型 type RegionTable struct { Region string Zones string @@ -62,7 +60,7 @@ type RegionTable struct { func getDefaultRegion() (string, string, error) { req := &uaccount.GetRegionRequest{} - resp, err := BizClient.GetRegion(req) + resp, err := base.BizClient.GetRegion(req) if err != nil { return "", "", err } @@ -77,34 +75,169 @@ func getDefaultRegion() (string, string, error) { return "", "", fmt.Errorf("No default region") } -func fetchRegion() (map[string][]string, error) { - req := &uaccount.GetRegionRequest{} - resp, err := BizClient.GetRegion(req) +// Region region, zone, isDefault +type Region struct { + Labels map[string][]string + DefaultRegion string + DefaultZone string +} + +func fetchRegion() (*Region, error) { + req := base.BizClient.NewGetRegionRequest() + resp, err := base.BizClient.GetRegion(req) if err != nil { return nil, err } - regionMap := make(map[string][]string) - for _, region := range resp.Regions { - regionMap[region.Region] = append(regionMap[region.Region], region.Zone) + region := &Region{ + Labels: make(map[string][]string), + } + for _, r := range resp.Regions { + region.Labels[r.Region] = append(region.Labels[r.Region], r.Zone) + if r.IsDefault { + region.DefaultRegion = r.Region + region.DefaultZone = r.Zone + } + } + return region, nil +} + +func fetchRegionWithConfig(cfg *base.AggConfig) (*Region, error) { + bc, err := base.GetBizClient(cfg) + req := bc.NewGetRegionRequest() + if err != nil { + return nil, err + } + resp, err := bc.GetRegion(req) + if err != nil { + return nil, err + } + region := &Region{ + Labels: make(map[string][]string), + } + for _, r := range resp.Regions { + region.Labels[r.Region] = append(region.Labels[r.Region], r.Zone) + if r.IsDefault { + region.DefaultRegion = r.Region + region.DefaultZone = r.Zone + } + } + return region, nil +} + +func getAllRegions() ([]string, error) { + regionIns, err := fetchRegion() + if err != nil { + return nil, err + } + list := []string{} + for region := range regionIns.Labels { + list = append(list, region) + } + return list, nil +} + +// 仅在命令补全中使用,忽略错误 +func getRegionList() []string { + regionIns, err := fetchRegion() + if err != nil { + return nil + } + list := []string{} + for region := range regionIns.Labels { + list = append(list, region) } - return regionMap, nil + return list } +func getZoneList(region string) []string { + regionIns, err := fetchRegion() + if err != nil { + return nil + } + list := []string{} + if region == "" { + for _, zones := range regionIns.Labels { + list = append(list, zones...) + } + } else { + list = regionIns.Labels[region] + } + return list +} + +var errNoDefaultProject = errors.New("No default project") + func getDefaultProject() (string, string, error) { - req := BizClient.NewGetProjectListRequest() - resp, err := BizClient.GetProjectList(req) + req := base.BizClient.NewGetProjectListRequest() + + resp, err := base.BizClient.GetProjectList(req) if err != nil { return "", "", err } - if resp.RetCode != 0 { - return "", "", fmt.Errorf("Something wrong. RetCode:%d, Message:%s", resp.RetCode, resp.Message) + for _, project := range resp.ProjectSet { + if project.IsDefault == true { + return project.ProjectId, project.ProjectName, nil + } + } + return "", "", errNoDefaultProject +} + +func getDefaultProjectWithConfig(cfg *base.AggConfig) (string, string, error) { + bc, err := base.GetBizClient(cfg) + if err != nil { + return "", "", err + } + + req := bc.NewGetProjectListRequest() + resp, err := bc.GetProjectList(req) + if err != nil { + return "", "", err } for _, project := range resp.ProjectSet { if project.IsDefault == true { return project.ProjectId, project.ProjectName, nil } } - return "", "", fmt.Errorf("No default project") + return "", "", errNoDefaultProject +} + +func fetchProjectWithConfig(cfg *base.AggConfig) (map[string]bool, error) { + bc, err := base.GetBizClient(cfg) + if err != nil { + return nil, err + } + + req := bc.NewGetProjectListRequest() + resp, err := bc.GetProjectList(req) + if err != nil { + return nil, err + } + + projects := map[string]bool{} + for _, project := range resp.ProjectSet { + projects[project.ProjectId] = true + } + return projects, nil +} + +func getReasonableProject(cfg *base.AggConfig) (string, error) { + if cfg.ProjectID == "" { + id, _, err := getDefaultProjectWithConfig(cfg) + if err != nil { + return "", fmt.Errorf("fetch project failed: %v", err) + } + return id, nil + } + + projects, err := fetchProjectWithConfig(cfg) + if err != nil { + return "", fmt.Errorf("fetch project failed: %v", err) + } + if _, ok := projects[cfg.ProjectID]; !ok { + return "", fmt.Errorf("project[%s] does not exist", cfg.ProjectID) + } + + return cfg.ProjectID, nil } func isUserCertified(userInfo *uaccount.UserInfo) bool { @@ -112,9 +245,9 @@ func isUserCertified(userInfo *uaccount.UserInfo) bool { } func getUserInfo() (*uaccount.UserInfo, error) { - req := BizClient.NewGetUserInfoRequest() + req := base.BizClient.NewGetUserInfoRequest() var userInfo uaccount.UserInfo - resp, err := BizClient.GetUserInfo(req) + resp, err := base.BizClient.GetUserInfo(req) if err != nil { return nil, err @@ -125,13 +258,11 @@ func getUserInfo() (*uaccount.UserInfo, error) { } if len(resp.DataSet) == 1 { userInfo = resp.DataSet[0] - Cxt.AppendInfo("userName", userInfo.UserEmail) - Cxt.AppendInfo("companyName", userInfo.CompanyName) bytes, err := json.Marshal(userInfo) if err != nil { return nil, err } - fileFullPath := GetConfigPath() + "/user.json" + fileFullPath := base.GetConfigDir() + "/user.json" err = ioutil.WriteFile(fileFullPath, bytes, 0600) if err != nil { return nil, err diff --git a/cmd/root.go b/cmd/root.go index fec3ce1117..283f9f4a7f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -17,40 +17,31 @@ package cmd import ( "fmt" "os" + "strconv" - "github.com/Sirupsen/logrus" "github.com/spf13/cobra" - . "github.com/ucloud/ucloud-cli/base" + "github.com/ucloud/ucloud-cli/base" + "github.com/ucloud/ucloud-sdk-go/ucloud/log" ) -//GlobalFlag 几乎所有接口都需要的参数,例如 region zone projectID -type GlobalFlag struct { - debug bool - json bool - version bool - completion bool - config bool - signup bool -} - -var global GlobalFlag +var global = &base.Global -//NewCmdRoot 创建rootCmd rootCmd represents the base command when called without any subcommands +// NewCmdRoot 创建rootCmd rootCmd represents the base command when called without any subcommands func NewCmdRoot() *cobra.Command { - var cmd = &cobra.Command{ - Use: "ucloud", - Short: "UCloud CLI v" + Version, - Long: `UCloud CLI - manage UCloud resources and developer workflow`, - BashCompletionFunction: "__ucloud_init_completion", + cmd := &cobra.Command{ + Use: "ucloud", + Short: "UCloud CLI v" + base.Version, + Long: `UCloud CLI - manage UCloud resources and developer workflow`, + DisableAutoGenTag: true, Run: func(cmd *cobra.Command, args []string) { - if global.version { - Cxt.Printf("ucloud cli %s\n", Version) - } else if global.completion { + if global.Version { + base.Cxt.Printf("ucloud cli %s\n", base.Version) + } else if global.Completion { NewCmdCompletion().Run(cmd, args) - } else if global.config { - config.ListConfig(global.json) - } else if global.signup { + } else if global.Config { + base.ListAggConfig(global.JSON) + } else if global.Signup { NewCmdSignup().Run(cmd, args) } else { cmd.HelpFunc()(cmd, args) @@ -58,25 +49,19 @@ func NewCmdRoot() *cobra.Command { }, } - cmd.PersistentFlags().BoolVarP(&global.debug, "debug", "d", false, "Running in debug mode") - cmd.PersistentFlags().BoolVarP(&global.json, "json", "j", false, "Print result in JSON format whenever possible") - cmd.Flags().BoolVar(&global.version, "version", false, "Display version") - cmd.Flags().BoolVar(&global.completion, "completion", false, "Turn on auto completion according to the prompt") - cmd.Flags().BoolVar(&global.config, "config", false, "Display configuration") - cmd.Flags().BoolVar(&global.signup, "signup", false, "Launch UCloud sign up page in browser") - - cmd.AddCommand(NewCmdInit()) - cmd.AddCommand(NewCmdConfig()) - cmd.AddCommand(NewCmdRegion()) - cmd.AddCommand(NewCmdProject()) - cmd.AddCommand(NewCmdUHost()) - cmd.AddCommand(NewCmdEIP()) - cmd.AddCommand(NewCmdGssh()) - cmd.AddCommand(NewCmdUImage()) - cmd.AddCommand(NewCmdSubnet()) - cmd.AddCommand(NewCmdVpc()) - cmd.AddCommand(NewCmdFirewall()) - cmd.AddCommand(NewCmdDisk()) + cmd.PersistentFlags().BoolVarP(&global.Debug, "debug", "d", false, "Running in debug mode") + cmd.PersistentFlags().BoolVarP(&global.JSON, "json", "j", false, "Print result in JSON format whenever possible") + cmd.PersistentFlags().StringVarP(&global.Profile, "profile", "p", global.Profile, "Specifies the configuration for the operation") + cmd.Flags().BoolVarP(&global.Version, "version", "v", false, "Display version") + cmd.Flags().BoolVar(&global.Completion, "completion", false, "Turn on auto completion according to the prompt") + cmd.Flags().BoolVar(&global.Config, "config", false, "Display configuration") + cmd.Flags().BoolVar(&global.Signup, "signup", false, "Launch UCloud sign up page in browser") + + cmd.PersistentFlags().SetFlagValuesFunc("profile", func() []string { return base.AggConfigListIns.GetProfileNameList() }) + cmd.SetHelpTemplate(helpTmpl) + cmd.SetUsageTemplate(usageTmpl) + resetHelpFunc(cmd) + return cmd } @@ -110,32 +95,120 @@ Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} ` -//概要帮助信息模板 +// 概要帮助信息模板 const usageTmpl = `Usage:{{if .Runnable}} {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} [command] {{if $size:=len .Commands}} - {{"command may be" | printf "%-20s"}} {{range $index,$cmd:= .Commands}}{{if .IsAvailableCommand}}{{$cmd.Name}}{{if gt $size (add $index 2)}} | {{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableFlags}} + {{"command may be" | printf "%-20s"}} {{range $index,$cmd:= .Commands}}{{if .IsAvailableCommand}}{{$cmd.Name}}{{if gt $size (add $index 1)}} | {{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableFlags}} {{"flags may be" | printf "%-20s"}} {{.Flags.FlagNames}} Use "{{.CommandPath}} --help" for details.{{end}} ` +func addChildren(root *cobra.Command) { + out := base.Cxt.GetWriter() + root.AddCommand(NewCmdInit()) + root.AddCommand(NewCmdDoc(out)) + root.AddCommand(NewCmdConfig()) + root.AddCommand(NewCmdRegion(out)) + root.AddCommand(NewCmdProject()) + root.AddCommand(NewCmdUHost()) + root.AddCommand(NewCmdUPHost()) + root.AddCommand(NewCmdUImage()) + root.AddCommand(NewCmdSubnet()) + root.AddCommand(NewCmdVpc()) + root.AddCommand(NewCmdFirewall()) + root.AddCommand(NewCmdDisk()) + root.AddCommand(NewCmdEIP()) + root.AddCommand(NewCmdBandwidth()) + root.AddCommand(NewCmdUDPN(out)) + root.AddCommand(NewCmdULB()) + root.AddCommand(NewCmdGssh()) + root.AddCommand(NewCmdPathx()) + root.AddCommand(NewCmdMysql()) + root.AddCommand(NewCmdRedis()) + root.AddCommand(NewCmdMemcache()) + root.AddCommand(NewCmdExt()) + root.AddCommand(NewCmdAPI(out)) + root.AddCommand(NewCmdSignature()) + for _, c := range root.Commands() { + if c.Name() != "init" && c.Name() != "gendoc" && c.Name() != "config" { + c.PersistentFlags().StringVar(&global.PublicKey, "public-key", global.PublicKey, "Set public-key to override the public-key in local config file") + c.PersistentFlags().StringVar(&global.PrivateKey, "private-key", global.PrivateKey, "Set private-key to override the private-key in local config file") + c.PersistentFlags().StringVar(&global.BaseURL, "base-url", "", "Set base-url to override the base-url in local config file") + c.PersistentFlags().IntVar(&global.Timeout, "timeout-sec", 0, "Set timeout-sec to override the timeout-sec in local config file") + c.PersistentFlags().IntVar(&global.MaxRetryTimes, "max-retry-times", -1, "Set max-retry-times to override the max-retry-times in local config file") + } + } +} + // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { - rootCmd := NewCmdRoot() - rootCmd.SetHelpTemplate(helpTmpl) - rootCmd.SetUsageTemplate(usageTmpl) - resetHelpFunc(rootCmd) + cmd := NewCmdRoot() + if base.InCloudShell { + err := base.InitConfigInCloudShell() + if err != nil { + base.HandleError(err) + return + } + } + base.InitConfig() + mode := os.Getenv("UCLOUD_CLI_DEBUG") + if mode == "on" || global.Debug { + base.ClientConfig.LogLevel = log.DebugLevel + base.BizClient = base.NewClient(base.ClientConfig, base.AuthCredential) + } + + addChildren(cmd) + + targetCmd, flags, err := cmd.Find(os.Args[1:]) + if err == nil { + if targetCmd.Use == "api" { + targetCmd.Run(targetCmd, flags) + return + } + } - if err := rootCmd.Execute(); err != nil { + if err := cmd.Execute(); err != nil { os.Exit(1) } } func init() { + //-1表示不覆盖配置文件中的MaxRetryTimes参数 + global.MaxRetryTimes = -1 + for idx, arg := range os.Args { + if arg == "--profile" && len(os.Args) > idx+1 && os.Args[idx+1] != "" { + global.Profile = os.Args[idx+1] + } + if arg == "--public-key" && len(os.Args) > idx+1 && os.Args[idx+1] != "" { + global.PublicKey = os.Args[idx+1] + } + if arg == "--private-key" && len(os.Args) > idx+1 && os.Args[idx+1] != "" { + global.PrivateKey = os.Args[idx+1] + } + if arg == "--base-url" && len(os.Args) > idx+1 && os.Args[idx+1] != "" { + global.BaseURL = os.Args[idx+1] + } + if arg == "--timeout-sec" && len(os.Args) > idx+1 && os.Args[idx+1] != "" { + sec, err := strconv.Atoi(os.Args[idx+1]) + if err != nil { + fmt.Printf("parse timeout-sec failed: %v\n", err) + } else { + global.Timeout = sec + } + } + if arg == "--max-retry-times" && len(os.Args) > idx+1 && os.Args[idx+1] != "" { + times, err := strconv.Atoi(os.Args[idx+1]) + if err != nil { + fmt.Printf("parse max-retry-times failed: %v\n", err) + } else { + global.MaxRetryTimes = times + } + } + } cobra.EnableCommandSorting = false cobra.OnInitialize(initialize) - Cxt.AppendInfo("command", fmt.Sprintf("%v", os.Args)) } func resetHelpFunc(cmd *cobra.Command) { @@ -147,25 +220,32 @@ func resetHelpFunc(cmd *cobra.Command) { } func initialize(cmd *cobra.Command) { - if global.debug { - logrus.SetLevel(logrus.DebugLevel) + flags := cmd.Flags() + project, err := flags.GetString("project-id") + if err == nil { + base.ClientConfig.ProjectId = project } - userInfo, err := LoadUserInfo() + region, err := flags.GetString("region") if err == nil { - Cxt.AppendInfo("userName", userInfo.UserEmail) - Cxt.AppendInfo("companyName", userInfo.CompanyName) - } else { - Cxt.PrintErr(err) + base.ClientConfig.Region = region + } + + zone, err := flags.GetString("zone") + if err == nil { + base.ClientConfig.Zone = zone } if (cmd.Name() != "config" && cmd.Name() != "init" && cmd.Name() != "version") && (cmd.Parent() != nil && cmd.Parent().Name() != "config") { - if config.PrivateKey == "" { - Cxt.Println("private-key is empty. Execute command 'ucloud init' or 'ucloud config' to configure your private-key") + if base.InCloudShell { + return + } + if base.ConfigIns.PrivateKey == "" { + base.Cxt.Println("private-key is empty. Execute command 'ucloud init|config' to configure it or run 'ucloud config list' to check your configurations") os.Exit(0) } - if config.PublicKey == "" { - Cxt.Println("public-key is empty. Execute command 'ucloud init' or 'ucloud config' to configure your public-key") + if base.ConfigIns.PublicKey == "" { + base.Cxt.Println("public-key is empty. Execute command 'ucloud init|config' to configure it or run 'ucloud config list' to check your configurations") os.Exit(0) } } diff --git a/cmd/root_test.go b/cmd/root_test.go deleted file mode 100644 index 4529a1d7da..0000000000 --- a/cmd/root_test.go +++ /dev/null @@ -1,8 +0,0 @@ -package cmd - -import "testing" - -func TestCmdRoot(t *testing.T) { - root := NewCmdRoot() - root.Execute() -} diff --git a/cmd/signature.go b/cmd/signature.go new file mode 100644 index 0000000000..1e4bcc2c53 --- /dev/null +++ b/cmd/signature.go @@ -0,0 +1,93 @@ +package cmd + +import ( + "bytes" + "fmt" + "net/url" + "strings" + + "github.com/fatih/color" + "github.com/spf13/cobra" + "github.com/ucloud/ucloud-sdk-go/ucloud/auth" +) + +func NewCmdSignature() *cobra.Command { + var ( + rawParams []string + privateKey string + rawURL string + ) + cmd := &cobra.Command{ + Use: "signature", + Short: "Calculate ucloud signature", + Long: "Calculate ucloud signature", + + Aliases: []string{"sign"}, + + Run: func(cmd *cobra.Command, args []string) { + var params map[string]interface{} + if rawURL != "" { + // Parse params from exists url + parsedURL, err := url.Parse(rawURL) + if err != nil { + fmt.Printf("error: failed to parse url %q: %v\n", rawURL, err) + return + } + query := parsedURL.Query() + params = make(map[string]interface{}, len(query)) + for key, values := range query { + if key == "Signature" { + fmt.Println("error: the `Signature` cannot be placed in url") + return + } + if len(values) == 0 { + continue + } + val := values[0] + params[key] = val + } + } + if len(rawParams) > 0 { + if params == nil { + params = make(map[string]interface{}, len(rawParams)) + } + for _, rawParam := range rawParams { + kv := strings.Split(rawParam, "=") + if len(kv) != 2 { + fmt.Printf("error: param %q is invalid\n", rawParam) + return + } + params[kv[0]] = kv[1] + } + } + if len(params) == 0 { + fmt.Println("error: missing param") + return + } + + r := auth.CalculateSignature(params, privateKey) + + var colorParamBuf bytes.Buffer + for _, key := range r.SortedKeys { + val := params[key] + colorParamBuf.WriteString(color.GreenString(key)) + colorParamBuf.WriteString(color.CyanString("%v", val)) + } + colorParamBuf.WriteString(color.MagentaString(privateKey)) + fmt.Println("") + fmt.Printf("ParamStr: %s\n", colorParamBuf.String()) + fmt.Println("") + + fmt.Printf("Signature: %s\n", color.BlueString(r.Sign)) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + flags.StringArrayVarP(&rawParams, "param", "m", nil, "Request params") + flags.StringVarP(&privateKey, "private-key", "k", "", "Private key") + flags.StringVarP(&rawURL, "url", "u", "", "Request url without signature") + cmd.MarkFlagRequired("private-key") + + return cmd +} diff --git a/cmd/signup.go b/cmd/signup.go index 6f5cb2546d..4a3ffd9391 100644 --- a/cmd/signup.go +++ b/cmd/signup.go @@ -22,7 +22,7 @@ import ( "github.com/spf13/cobra" ) -//NewCmdSignup ucloud signup +// NewCmdSignup ucloud signup func NewCmdSignup() *cobra.Command { var cmd = &cobra.Command{ Use: "signup", diff --git a/cmd/udb.go b/cmd/udb.go new file mode 100644 index 0000000000..433386d3db --- /dev/null +++ b/cmd/udb.go @@ -0,0 +1,639 @@ +// Copyright © 2018 NAME HERE tony.li@ucloud.cn +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "bufio" + "encoding/base64" + "fmt" + "io" + "os" + "strconv" + "strings" + + "github.com/spf13/cobra" + + "github.com/ucloud/ucloud-sdk-go/services/udb" + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + + "github.com/ucloud/ucloud-cli/base" + "github.com/ucloud/ucloud-cli/model/status" +) + +// NewCmdUDBConf ucloud udb conf +func NewCmdUDBConf() *cobra.Command { + cmd := &cobra.Command{ + Use: "conf", + Short: "List and manipulate configuration files of MySQL instances", + Long: "List and manipulate configuration files of MySQL instances", + } + out := base.Cxt.GetWriter() + cmd.AddCommand(NewCmdUDBConfList(out)) + cmd.AddCommand(NewCmdUDBConfDescribe(out)) + cmd.AddCommand(NewCmdUDBConfClone(out)) + cmd.AddCommand(NewCmdUDBConfUpload(out)) + cmd.AddCommand(NewCmdUDBConfUpdate(out)) + cmd.AddCommand(NewCmdUDBConfDelete(out)) + cmd.AddCommand(NewCmdUDBConfApply(out)) + cmd.AddCommand(NewCmdUDBConfDownload(out)) + return cmd +} + +// UDBConfRow 表格行 +type UDBConfRow struct { + ConfID int + DBVersion string + Name string + Description string + Modifiable bool + Zone string +} + +var dbTypeMap = map[string]string{ + "mysql": "sql", + "mongodb": "nosql", + "postgresql": "postgresql", + "sqlserver": "sqlserver", +} + +var dbTypeList = []string{"mysql", "mongodb", "postgresql", "sqlserver"} + +// NewCmdUDBConfList ucloud mysql conf list +func NewCmdUDBConfList(out io.Writer) *cobra.Command { + req := base.BizClient.NewDescribeUDBParamGroupRequest() + cmd := &cobra.Command{ + Use: "list", + Short: "List configuartion files of MySQL instances", + Long: "List configuartion files of MySQL instances", + Run: func(c *cobra.Command, args []string) { + if *req.GroupId == 0 { + req.GroupId = nil + } + resp, err := base.BizClient.DescribeUDBParamGroup(req) + if err != nil { + base.HandleError(err) + return + } + list := []UDBConfRow{} + for _, ins := range resp.DataSet { + row := UDBConfRow{ + ConfID: ins.GroupId, + Name: ins.GroupName, + Zone: ins.Zone, + DBVersion: ins.DBTypeId, + Description: ins.Description, + Modifiable: ins.Modifiable, + } + list = append(list, row) + } + base.PrintList(list, out) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + bindRegion(req, flags) + bindZone(req, flags) + bindProjectID(req, flags) + bindOffset(req, flags) + bindLimit(req, flags) + req.GroupId = flags.Int("conf-id", 0, "Optional. Configuration identifier for the configuration to be described") + req.ClassType = sdk.String("sql") + + flags.SetFlagValuesFunc("conf-id", func() []string { + return getConfIDList(*req.ClassType, *req.ProjectId, *req.Region, *req.Zone) + }) + + return cmd +} + +// UDBConfParamRow 参数配置展示表格行 +type UDBConfParamRow struct { + Key string + Value string +} + +// NewCmdUDBConfDescribe ucloud udb conf describe +func NewCmdUDBConfDescribe(out io.Writer) *cobra.Command { + var confID string + req := base.BizClient.NewDescribeUDBParamGroupRequest() + req.RegionFlag = sdk.Bool(false) + cmd := &cobra.Command{ + Use: "describe", + Short: "Display details about a configuration file of MySQL instance", + Long: "Display details about a configuration file of MySQL instance", + Run: func(c *cobra.Command, args []string) { + id, err := strconv.Atoi(base.PickResourceID(confID)) + if err != nil { + base.HandleError(err) + return + } + req.GroupId = &id + resp, err := base.BizClient.DescribeUDBParamGroup(req) + if err != nil { + base.HandleError(err) + return + } + if len(resp.DataSet) != 1 { + fmt.Fprintf(out, "Error, conf-id[%d] may not be exist\n", req.GroupId) + return + } + conf := resp.DataSet[0] + attrs := []base.DescribeTableRow{ + {Attribute: "ConfID", Content: strconv.Itoa(conf.GroupId)}, + {Attribute: "DBVersion", Content: conf.DBTypeId}, + {Attribute: "Name", Content: conf.GroupName}, + {Attribute: "Description", Content: conf.Description}, + {Attribute: "Modifiable", Content: strconv.FormatBool(conf.Modifiable)}, + {Attribute: "Zone", Content: conf.Zone}, + } + fmt.Fprintln(out, "Attributes:") + base.PrintList(attrs, out) + + params := []UDBConfParamRow{} + for _, p := range conf.ParamMember { + if p.Value == "" { + continue + } + row := UDBConfParamRow{ + Key: p.Key, + Value: p.Value, + } + params = append(params, row) + } + fmt.Fprintln(out, "\nParameters:") + base.PrintList(params, out) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringVar(&confID, "conf-id", "", "Requried. Configuration identifier for the configuration to be described") + bindRegion(req, flags) + bindZone(req, flags) + bindProjectID(req, flags) + + cmd.MarkFlagRequired("conf-id") + flags.SetFlagValuesFunc("conf-id", func() []string { + return getConfIDList("sql", *req.ProjectId, *req.Region, *req.Zone) + }) + + return cmd +} + +// NewCmdUDBConfClone ucloud udb conf clone +func NewCmdUDBConfClone(out io.Writer) *cobra.Command { + var srcConfID string + req := base.BizClient.NewCreateUDBParamGroupRequest() + cmd := &cobra.Command{ + Use: "clone", + Short: "Create configuration file by cloning existed configuration", + Long: "Create configuration file by cloning existed configuration", + Run: func(c *cobra.Command, args []string) { + id, err := strconv.Atoi(base.PickResourceID(srcConfID)) + if err != nil { + base.HandleError(err) + return + } + if *req.DBTypeId == "" { + confIns, err := getConfByID(id, *req.ProjectId, *req.Region, *req.Zone) + if err != nil { + base.HandleError(err) + return + } + req.DBTypeId = sdk.String(confIns.DBTypeId) + } + req.SrcGroupId = &id + resp, err := base.BizClient.CreateUDBParamGroup(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintf(out, "conf[%d] created\n", resp.GroupId) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + req.DBTypeId = flags.String("db-version", "", fmt.Sprintf("Required. Version of DB. Accept values:%s", strings.Join(dbVersionList, ", "))) + req.GroupName = flags.String("name", "", "Required. Name of configuration. It's length should be between 6 and 63") + req.Description = flags.String("description", " ", "Optional. Description of the configuration to clone") + bindRegion(req, flags) + bindZone(req, flags) + bindProjectID(req, flags) + flags.StringVar(&srcConfID, "src-conf-id", "", "Optional. The ConfID of source configuration which to be cloned from") + + flags.SetFlagValues("db-version", dbVersionList...) + flags.SetFlagValuesFunc("src-conf-id", func() []string { + return getConfIDList("sql", *req.ProjectId, *req.Region, *req.Zone) + }) + + cmd.MarkFlagRequired("name") + cmd.MarkFlagRequired("src-conf-id") + return cmd +} + +var udbSubtypeMap = map[string]int{ + "unknow": 0, + "Shardsvr-MMAPv1": 1, + "Shardsvr-WiredTiger": 2, + "Configsvr-MMAPv1": 3, + "Configsvr-WiredTiger": 4, + "Mongos": 5, + "Mysql": 10, + "Postgresql": 20, +} + +var subtypeList = []string{"Shardsvr-MMAPv1", "Shardsvr-WiredTiger", "Configsvr-MMAPv1", "Configsvr-WiredTiger", "Mongos", "Mysql", "Postgresql"} + +// NewCmdUDBConfUpload ucloud udb conf upload +func NewCmdUDBConfUpload(out io.Writer) *cobra.Command { + var file string + req := base.BizClient.NewUploadUDBParamGroupRequest() + cmd := &cobra.Command{ + Use: "upload", + Short: "Create configuration file by uploading local DB configuration file", + Long: "Create configuration file by uploading local DB configuration file", + Run: func(c *cobra.Command, args []string) { + content, err := readFile(file) + if err != nil { + base.HandleError(err) + return + } + if l := len(*req.GroupName); l < 6 || l > 63 { + fmt.Fprintln(out, "Error, length of name shoud be between 6 and 63") + return + } + req.Content = sdk.String(base64.StdEncoding.EncodeToString([]byte(content))) + req.ParamGroupTypeId = sdk.Int(10) + resp, err := base.BizClient.UploadUDBParamGroup(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintf(out, "conf[%d] uploaded\n", resp.GroupId) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringVar(&file, "conf-file", "", "Required. Path of local configuration file") + req.DBTypeId = flags.String("db-version", "", fmt.Sprintf("Required. Version of DB. Accept values:%s", strings.Join(dbVersionList, ", "))) + req.GroupName = flags.String("name", "", "Required. Name of configuration. It's length should be between 6 and 63") + req.Description = flags.String("description", " ", "Optional. Description of the configuration to clone") + // flags.StringVar(&subtype, "db-type", "", fmt.Sprintf("Optional. DB type. Accept values: %s", strings.Join(subtypeList, ", "))) + bindRegion(req, flags) + bindZone(req, flags) + bindProjectID(req, flags) + + cmd.MarkFlagRequired("conf-file") + cmd.MarkFlagRequired("name") + cmd.MarkFlagRequired("db-version") + // cmd.MarkFlagRequired("db-type") + + flags.SetFlagValues("db-version", dbVersionList...) + // flags.SetFlagValues("db-type", subtypeList...) + flags.SetFlagValuesFunc("conf-file", func() []string { + return base.GetFileList("") + }) + return cmd +} + +// NewCmdUDBConfUpdate ucloud udb conf update +func NewCmdUDBConfUpdate(out io.Writer) *cobra.Command { + var confID, key, value, file string + req := base.BizClient.NewUpdateUDBParamGroupRequest() + cmd := &cobra.Command{ + Use: "update", + Short: "Update parameters of DB's configuration", + Long: "Update parameters of DB's configuration", + Run: func(c *cobra.Command, args []string) { + id, err := strconv.Atoi(base.PickResourceID(confID)) + if err != nil { + base.HandleError(err) + return + } + req.GroupId = &id + + if key != "" && value != "" { + req.Key = &key + req.Value = &value + _, err := base.BizClient.UpdateUDBParamGroup(req) + if err != nil { + base.HandleError(err) + } else { + fmt.Printf("conf[%s]'sparameter[%s = %s] updated\n", confID, key, value) + } + } + if file != "" { + params, err := parseParam(file) + if err != nil { + base.HandleError(err) + return + } + for _, p := range params { + req.Key = sdk.String(p.Key) + req.Value = sdk.String(p.Value) + _, err := base.BizClient.UpdateUDBParamGroup(req) + if err != nil { + fmt.Printf("conf[%s]'sparameter[%s = %s] failed\n", confID, p.Key, p.Value) + base.HandleError(err) + } else { + fmt.Printf("conf[%s]'sparameter[%s = %s] updated\n", confID, p.Key, p.Value) + } + fmt.Println("") + } + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + bindRegion(req, flags) + bindZone(req, flags) + bindProjectID(req, flags) + + flags.StringVar(&confID, "conf-id", "", "Required. ConfID of configuration to update") + flags.StringVar(&key, "key", "", "Optional. Key of parameter") + flags.StringVar(&value, "value", "", "Optional. Value of parameter") + flags.StringVar(&file, "file", "", "Optional. Path of file in which each parameter occupies one line with format 'key = value'") + + flags.SetFlagValuesFunc("conf-id", func() []string { + return getModifiableConfIDList("", *req.ProjectId, *req.Region, *req.Zone) + }) + flags.SetFlagValuesFunc("file", func() []string { + return base.GetFileList("") + }) + + cmd.MarkFlagRequired("conf-id") + return cmd +} + +// NewCmdUDBConfDelete ucloud udb conf delete +func NewCmdUDBConfDelete(out io.Writer) *cobra.Command { + var confID string + req := base.BizClient.NewDeleteUDBParamGroupRequest() + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete configuration of udb by conf-id", + Long: "Delete configuration of udb by conf-id", + Run: func(c *cobra.Command, args []string) { + id, err := strconv.Atoi(base.PickResourceID(confID)) + if err != nil { + base.HandleError(err) + return + } + req.GroupId = &id + _, err = base.BizClient.DeleteUDBParamGroup(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintf(out, "conf[%s] deleted\n", confID) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringVar(&confID, "conf-id", "", "Required. ConfID of the configuration to delete") + bindRegion(req, flags) + bindZone(req, flags) + bindProjectID(req, flags) + + cmd.MarkFlagRequired("conf-id") + flags.SetFlagValuesFunc("conf-id", func() []string { + return getModifiableConfIDList("", *req.ProjectId, *req.Region, *req.Zone) + }) + return cmd +} + +// NewCmdUDBConfApply ucloud udb conf apply +func NewCmdUDBConfApply(out io.Writer) *cobra.Command { + var confID string + var udbIDs []string + var restart, yes, async bool + + req := base.BizClient.UDBClient.NewChangeUDBParamGroupRequest() + cmd := &cobra.Command{ + Use: "apply", + Short: "Apply configuration for UDB instances", + Long: "Apply configuration for UDB instances", + Run: func(c *cobra.Command, args []string) { + req.GroupId = sdk.String(base.PickResourceID(confID)) + for _, idname := range udbIDs { + req.DBId = sdk.String(base.PickResourceID(idname)) + _, err := base.BizClient.UDBClient.ChangeUDBParamGroup(req) + if err != nil { + base.HandleError(err) + continue + } + fmt.Fprintf(out, "conf[%s] has applied for udb[%s]\n", confID, idname) + if !restart { + continue + } + ok := base.Confirm(yes, fmt.Sprintf("udb[%s] is about to restart, do you want to continue?", idname)) + if !ok { + continue + } + restartReq := base.BizClient.NewRestartUDBInstanceRequest() + restartReq.Region = req.Region + restartReq.Zone = req.Zone + restartReq.ProjectId = req.ProjectId + restartReq.DBId = req.DBId + _, err = base.BizClient.RestartUDBInstance(restartReq) + if err != nil { + base.HandleError(err) + continue + } + if async { + fmt.Fprintf(out, "udb[%s] is restarting\n", idname) + } else { + text := fmt.Sprintf("udb[%s] is restarting", idname) + poller.Spoll(*req.DBId, text, []string{status.UDB_FAIL, status.UDB_RUNNING}) + } + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringVar(&confID, "conf-id", "", "Required. ConfID of the configuration to be applied") + flags.StringSliceVar(&udbIDs, "udb-id", nil, "Required. Resource ID of UDB instances to change configuration") + flags.BoolVar(&restart, "restart-after-apply", true, "Optional. The new configuration will take effect after DB restarts") + flags.BoolVarP(&yes, "yes", "y", false, "Optional. Do not prompt for confirmation") + flags.BoolVarP(&async, "async", "a", false, "Optional. Do not wait for the long-running operation to finish.") + bindRegion(req, flags) + bindZone(req, flags) + bindProjectID(req, flags) + + cmd.MarkFlagRequired("conf-id") + cmd.MarkFlagRequired("udb-id") + + flags.SetFlagValuesFunc("conf-id", func() []string { + return getModifiableConfIDList("", *req.ProjectId, *req.Region, *req.Zone) + }) + flags.SetFlagValuesFunc("udb-id", func() []string { + return getUDBIDList(nil, "", *req.ProjectId, *req.Region, *req.Zone) + }) + + return cmd +} + +// NewCmdUDBConfDownload ucloud udb conf download +func NewCmdUDBConfDownload(out io.Writer) *cobra.Command { + var confID string + req := base.BizClient.UDBClient.NewExtractUDBParamGroupRequest() + cmd := &cobra.Command{ + Use: "download", + Short: "Download UDB configuration", + Long: "Download UDB configuration", + Run: func(c *cobra.Command, args []string) { + id, err := strconv.Atoi(base.PickResourceID(confID)) + if err != nil { + base.HandleError(err) + return + } + + req.GroupId = &id + resp, err := base.BizClient.UDBClient.ExtractUDBParamGroup(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprint(out, resp.Content) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringVar(&confID, "conf-id", "", "Required. ConfID of configuration to download") + bindRegion(req, flags) + bindZone(req, flags) + bindProjectID(req, flags) + + cmd.MarkFlagRequired("conf-id") + + flags.SetFlagValuesFunc("conf-id", func() []string { + return getConfIDList("sql", *req.ProjectId, *req.Region, *req.Zone) + }) + + return cmd +} + +type confParam struct { + Key string + Value string +} + +func parseParam(filePath string) ([]confParam, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + params := []confParam{} + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + line = strings.TrimSpace(line) + if len(line) == 0 { + continue + } + strs := strings.SplitN(line, "=", 2) + if len(strs) < 2 { + continue + } + param := confParam{ + Key: strings.TrimSpace(strs[0]), + Value: strings.TrimSpace(strs[1]), + } + params = append(params, param) + } + if err := scanner.Err(); err != nil { + return nil, err + } + return params, nil +} + +func getConfByID(confID int, project, region, zone string) (*udb.UDBParamGroupSet, error) { + req := base.BizClient.NewDescribeUDBParamGroupRequest() + req.ProjectId = &project + req.Region = ®ion + req.Zone = &zone + req.GroupId = &confID + resp, err := base.BizClient.DescribeUDBParamGroup(req) + if err != nil { + return nil, err + } + if len(resp.DataSet) != 1 { + return nil, fmt.Errorf("conf-id[%d] may not exist", *req.GroupId) + } + return &resp.DataSet[0], nil +} + +func getConfList(dbType, project, region, zone string) ([]udb.UDBParamGroupSet, error) { + req := base.BizClient.NewDescribeUDBParamGroupRequest() + req.ClassType = &dbType + req.ProjectId = &project + req.Region = ®ion + req.Zone = &zone + list := []udb.UDBParamGroupSet{} + for offset, limit := 0, 50; ; offset += limit { + req.Offset = sdk.Int(offset) + req.Limit = sdk.Int(limit) + resp, err := base.BizClient.DescribeUDBParamGroup(req) + if err != nil { + return nil, err + } + for _, conf := range resp.DataSet { + list = append(list, conf) + } + if resp.TotalCount <= offset+limit { + break + } + } + return list, nil +} + +func getModifiableConfIDList(dbType, project, region, zone string) []string { + confs, err := getConfList(dbType, project, region, zone) + if err != nil { + return nil + } + list := []string{} + for _, conf := range confs { + if conf.Modifiable == true { + list = append(list, fmt.Sprintf("%d/%s", conf.GroupId, conf.GroupName)) + } + } + return list +} + +func getConfIDList(dbType, project, region, zone string) []string { + confs, err := getConfList(dbType, project, region, zone) + if err != nil { + return nil + } + list := []string{} + for _, conf := range confs { + list = append(list, fmt.Sprintf("%d/%s", conf.GroupId, conf.GroupName)) + } + return list +} diff --git a/cmd/uhost.go b/cmd/uhost.go index d031297128..334410f382 100644 --- a/cmd/uhost.go +++ b/cmd/uhost.go @@ -15,21 +15,36 @@ package cmd import ( + "encoding/base64" + "errors" "fmt" - "net" + "io" + "regexp" "strings" + "sync" + "time" "github.com/spf13/cobra" "github.com/ucloud/ucloud-sdk-go/services/uhost" + "github.com/ucloud/ucloud-sdk-go/services/unet" sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + sdkerror "github.com/ucloud/ucloud-sdk-go/ucloud/error" + "github.com/ucloud/ucloud-sdk-go/ucloud/request" "github.com/ucloud/ucloud-cli/base" + "github.com/ucloud/ucloud-cli/model/cli" "github.com/ucloud/ucloud-cli/model/status" "github.com/ucloud/ucloud-cli/ux" ) -//NewCmdUHost ucloud uhost +const _RetCodeRegionNoPermission = 230 + +const _MaxBoundSecGroupCount = 5 + +var uhostSpoller = base.NewSpoller(sdescribeUHostByID, base.Cxt.GetWriter()) + +// NewCmdUHost ucloud uhost func NewCmdUHost() *cobra.Command { cmd := &cobra.Command{ Use: "uhost", @@ -37,281 +52,768 @@ func NewCmdUHost() *cobra.Command { Long: `List,create,delete,stop,restart,poweroff or resize UHost instance`, Args: cobra.NoArgs, } - cmd.AddCommand(NewCmdUHostList()) + out := base.Cxt.GetWriter() + cmd.AddCommand(NewCmdUHostList(out)) cmd.AddCommand(NewCmdUHostCreate()) cmd.AddCommand(NewCmdUHostDelete()) - cmd.AddCommand(NewCmdUHostStop()) - cmd.AddCommand(NewCmdUHostStart()) - cmd.AddCommand(NewCmdUHostReboot()) - cmd.AddCommand(NewCmdUHostPoweroff()) - cmd.AddCommand(NewCmdUHostResize()) + cmd.AddCommand(NewCmdUHostStop(out)) + cmd.AddCommand(NewCmdUHostStart(out)) + cmd.AddCommand(NewCmdUHostReboot(out)) + cmd.AddCommand(NewCmdUHostPoweroff(out)) + cmd.AddCommand(NewCmdUHostResize(out)) + cmd.AddCommand(NewCmdUHostClone(out)) + cmd.AddCommand(NewCmdUhostResetPassword(out)) + cmd.AddCommand(NewCmdUhostReinstallOS(out)) + cmd.AddCommand(NewCmdUhostCreateImage(out)) + cmd.AddCommand(NewCmdIsolation(out)) + cmd.AddCommand(NewCmdUhostLeaveIsolationGroup(out)) return cmd } -//UHostRow UHost表格行 +// UHostRow UHost表格行 type UHostRow struct { UHostName string + Remark string ResourceID string Group string PrivateIP string PublicIP string Config string + DiskSet string + Zone string + Image string + VPC string + Subnet string Type string State string CreationTime string } -//NewCmdUHostList [ucloud uhost list] -func NewCmdUHostList() *cobra.Command { +func listUhost(uhosts []uhost.UHostInstanceSet, out io.Writer, output string, listAllRegion bool) { + list := make([]UHostRow, 0) + for _, host := range uhosts { + row := UHostRow{} + row.UHostName = host.Name + row.Remark = host.Remark + row.ResourceID = host.UHostId + row.Group = host.Tag + for _, ip := range host.IPSet { + if row.PublicIP != "" { + row.PublicIP += " | " + } + if ip.Type == "Private" { + row.PrivateIP = ip.IP + row.VPC = ip.VPCId + row.Subnet = ip.SubnetId + } else { + row.PublicIP += fmt.Sprintf("%s", ip.IP) + } + } + cupCore := host.CPU + memorySize := host.Memory / 1024 + diskSize := 0 + var disks []string + for _, disk := range host.DiskSet { + if disk.Type == "Data" || disk.Type == "Udisk" { + diskSize += disk.Size + } + disks = append(disks, fmt.Sprintf("%s:%s:%dG", disk.Type, disk.DiskType, disk.Size)) + } + row.Zone = host.Zone + row.DiskSet = strings.Join(disks, "|") + row.Config = fmt.Sprintf("cpu:%d memory:%dG disk:%dG", cupCore, memorySize, diskSize) + row.Image = fmt.Sprintf("%s|%s", host.BasicImageId, host.BasicImageName) + row.CreationTime = base.FormatDate(host.CreateTime) + row.State = host.State + row.Type = host.MachineType + "/" + host.HostType + if host.HotplugFeature { + row.Type += "/HotPlug" + } + list = append(list, row) + } + if global.JSON { + base.PrintJSON(list, out) + } else { + var cols []string + if output == "wide" { + cols = []string{"UHostName", "Remark", "ResourceID", "Group", "PrivateIP", "PublicIP", "Config", "DiskSet", "Zone", "Image", "VPC", "Subnet", "Type", "State", "CreationTime"} + } else { + cols = []string{"UHostName", "ResourceID", "Group", "PrivateIP", "PublicIP", "Config", "Image", "Type", "State", "CreationTime"} + if listAllRegion { + cols = append(cols, "Zone") + } + } + base.PrintTable(list, cols) + } +} + +func listUhostID(uhosts []uhost.UHostInstanceSet, out io.Writer) { + ids := make([]string, 0) + for _, u := range uhosts { + ids = append(ids, u.UHostId) + } + fmt.Fprintln(out, strings.Join(ids, ",")) +} + +func fetchUHosts(req *uhost.DescribeUHostInstanceRequest) ([]uhost.UHostInstanceSet, int, error) { + resp, err := base.BizClient.DescribeUHostInstance(req) + if err != nil { + return nil, 0, err + } + return resp.UHostSet, resp.TotalCount, nil +} + +func fetchUHostsPageOff(req *uhost.DescribeUHostInstanceRequest) ([]uhost.UHostInstanceSet, error) { + _req := *req + result := make([]uhost.UHostInstanceSet, 0) + for limit, offset := 50, 0; ; offset += limit { + _req.Offset = sdk.Int(offset) + _req.Limit = sdk.Int(limit) + uhosts, total, err := fetchUHosts(&_req) + if err != nil { + return nil, err + } + result = append(result, uhosts...) + if offset+limit >= total { + break + } + } + return result, nil +} + +func getAllUHosts(req *uhost.DescribeUHostInstanceRequest, pageOff bool, allRegion bool) ([]uhost.UHostInstanceSet, error) { + if allRegion { + result := make([]uhost.UHostInstanceSet, 0) + regions, err := getAllRegions() + if err != nil { + return nil, err + } + for _, region := range regions { + _req := *req + _req.Region = sdk.String(region) + //如果要获取所有region的主机,则不分页 + uhosts, err := fetchUHostsPageOff(&_req) + // Has no permission in current region for UHost + if e, ok := err.(sdkerror.Error); ok && e.Code() == _RetCodeRegionNoPermission { + continue + } + if err != nil { + return nil, err + } + result = append(result, uhosts...) + } + return result, nil + } + + if pageOff { + _req := *req + uhosts, err := fetchUHostsPageOff(&_req) + if err != nil { + return nil, err + } + return uhosts, nil + } + + uhosts, _, err := fetchUHosts(req) + if err != nil { + return nil, err + } + return uhosts, nil +} + +// NewCmdUHostList [ucloud uhost list] +func NewCmdUHostList(out io.Writer) *cobra.Command { + var allRegion, pageOff, idOnly bool + var output string + var uhostIds []string req := base.BizClient.NewDescribeUHostInstanceRequest() cmd := &cobra.Command{ Use: "list", Short: "List all UHost Instances", Long: `List all UHost Instances`, Run: func(cmd *cobra.Command, args []string) { - resp, err := base.BizClient.DescribeUHostInstance(req) + *req.VPCId = base.PickResourceID(*req.VPCId) + *req.SubnetId = base.PickResourceID(*req.SubnetId) + *req.IsolationGroup = base.PickResourceID(*req.IsolationGroup) + for _, uhost := range uhostIds { + req.UHostIds = append(req.UHostIds, base.PickResourceID(uhost)) + } + + uhosts, err := getAllUHosts(req, pageOff, allRegion) if err != nil { base.HandleError(err) return } - if global.json { - base.PrintJSON(resp.UHostSet) + if idOnly { + listUhostID(uhosts, out) } else { - list := make([]UHostRow, 0) - for _, host := range resp.UHostSet { - row := UHostRow{} - row.UHostName = host.Name - row.ResourceID = host.UHostId - row.Group = host.Tag - for _, ip := range host.IPSet { - if row.PublicIP != "" { - row.PublicIP += " | " - } - if ip.Type == "Private" { - row.PrivateIP = ip.IP - } else { - row.PublicIP += fmt.Sprintf("%s %s", ip.IP, ip.Type) - } - } - osName := strings.SplitN(host.OsName, " ", 2) - cupCore := host.CPU - memorySize := host.Memory / 1024 - diskSize := 0 - for _, disk := range host.DiskSet { - if disk.Type == "Data" || disk.Type == "Udisk" { - diskSize += disk.Size - } - } - row.Config = fmt.Sprintf("%s cpu:%d memory:%dG disk:%dG", osName[0], cupCore, memorySize, diskSize) - row.CreationTime = base.FormatDate(host.CreateTime) - row.State = host.State - row.Type = host.UHostType + "/" + host.HostType - list = append(list, row) - } - base.PrintTableS(list) + listUhost(uhosts, out, output, allRegion) } }, } cmd.Flags().SortFlags = false - req.ProjectId = cmd.Flags().String("project-id", base.ConfigInstance.ProjectID, "Optional. Assign project-id") - req.Region = cmd.Flags().String("region", base.ConfigInstance.Region, "Optional. Assign region") + req.ProjectId = cmd.Flags().String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + req.Region = cmd.Flags().String("region", base.ConfigIns.Region, "Optional. Assign region.") req.Zone = cmd.Flags().String("zone", "", "Optional. Assign availability zone") - cmd.Flags().StringSliceVar(&req.UHostIds, "resource-id", make([]string, 0), "Optional. UHost Instance ID, multiple values separated by comma(without space)") - req.Tag = cmd.Flags().String("group", "", "Optional. Group") req.Offset = cmd.Flags().Int("offset", 0, "Optional. Offset default 0") req.Limit = cmd.Flags().Int("limit", 50, "Optional. Limit default 50, max value 100") + req.VPCId = cmd.Flags().String("vpc-id", "", "Optional. Resource ID of VPC. List uhost instances of the specified VPC") + req.SubnetId = cmd.Flags().String("subnet-id", "", "Optional. Resource ID of Subnet. List uhost instances of the specified Subnet") + req.IsolationGroup = cmd.Flags().String("isolation-group", "", "Optional. Resource ID of isolation group. List uhost instances of the specified isolation group") + cmd.Flags().StringSliceVar(&uhostIds, "uhost-id", make([]string, 0), "Optional. Resource ID of uhost instances, multiple values separated by comma(without space)") + cmd.Flags().BoolVar(&allRegion, "all-region", false, "Optional. Accpet values: true or false. List uhost instances of all regions when assigned true") + cmd.Flags().BoolVar(&pageOff, "page-off", false, "Optional. Paging or not. If all-region is specified this flag will be true. Accept values: true or false. If assigned, the limit flag will be disabled and list all uhost instances") + cmd.Flags().BoolVar(&idOnly, "uhost-id-only", false, "Optional. Just display resource id of uhost") + cmd.Flags().StringVarP(&output, "output", "o", "", "Optional. Accept values: wide. Display more information about uhost such as DiskSet and Zone") + bindGroup(req, cmd.Flags()) + + cmd.Flags().SetFlagValues("page-off", "true", "false") + cmd.Flags().SetFlagValues("uhost-id-only", "true", "false") + cmd.Flags().SetFlagValues("output", "wide") + cmd.Flags().SetFlagValuesFunc("project-id", getProjectList) + cmd.Flags().SetFlagValuesFunc("region", getRegionList) + cmd.Flags().SetFlagValuesFunc("zone", func() []string { + return getZoneList(req.GetRegion()) + }) + + flags := cmd.Flags() + flags.SetFlagValuesFunc("vpc-id", func() []string { + return getAllVPCIdNames(*req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("subnet-id", func() []string { + return getAllSubnetIDNames(*req.VPCId, *req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("isolation-group", func() []string { + return getIsolationGroupList(*req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("uhost-id", func() []string { + return getUhostList(nil, *req.ProjectId, *req.Region, *req.Zone) + }) return cmd } -//NewCmdUHostCreate [ucloud uhost create] +// NewCmdUHostCreate [ucloud uhost create] func NewCmdUHostCreate() *cobra.Command { - var bindEipID *string - var async *bool + var bindEipIDs []string + var hotPlug string + var async bool + var count int + var concurrent int + var hotPlugImageFlag bool + var userData string + var userDataImageFlag bool + var userDataBase64 string + var firewallId string + var secGroupIds []string + var keyPairId string + var password string req := base.BizClient.NewCreateUHostInstanceRequest() - eipReq := base.BizClient.NewAllocateEIPRequest() + eipReq := uhost.CreateUHostInstanceParamNetworkInterfaceEIP{} + updateEIPReq := base.BizClient.NewUpdateEIPAttributeRequest() cmd := &cobra.Command{ Use: "create", Short: "Create UHost instance", Long: "Create UHost instance", - Run: func(cmd *cobra.Command, args []string) { - *req.Memory *= 1024 - req.LoginMode = sdk.String("Password") - images := strings.SplitN(*req.ImageId, "/", 2) - if len(images) >= 2 { - *req.ImageId = images[0] - } - - resp, err := base.BizClient.CreateUHostInstance(req) - if err != nil { - base.HandleError(err) - return + PreRunE: func(cmd *cobra.Command, args []string) error { + if len(userData) > 0 && len(userDataBase64) > 0 { + return fmt.Errorf("%q conflicts with %q, can only set one of both", "user-data", "user-data-base64") } - if !*async { - if len(resp.UHostIds) == 1 { - text := fmt.Sprintf("UHost:[%s] is initializing", resp.UHostIds[0]) - done := pollUhost(resp.UHostIds[0], *req.ProjectId, *req.Region, *req.Zone, []string{status.HOST_RUNNING, status.HOST_FAIL}) - ux.DotSpinner.Start(text) - <-done - ux.DotSpinner.Stop() + if len(userDataBase64) > 0 { + if !base.IsBase64Encoded([]byte(userDataBase64)) { + return fmt.Errorf("%q must be base64-encoded", "user-data-base64") } - } else { - base.Cxt.Printf("UHost:%v created\n", resp.UHostIds) } - if *bindEipID != "" && len(resp.UHostIds) == 1 { - ip := net.ParseIP(*bindEipID) - if ip != nil { - eipID, err := getEIPIDbyIP(ip, *req.ProjectId, *req.Region) - if err != nil { - base.HandleError(err) - } else { - *bindEipID = eipID - } - } - bindEIP(sdk.String(resp.UHostIds[0]), sdk.String("uhost"), bindEipID, req.ProjectId, req.Region) + if concurrent > 50 { + return fmt.Errorf("%q should not be more than 50, current value is %v", "concurrent", concurrent) } + return nil + }, - if *eipReq.OperatorName != "" && *eipReq.Bandwidth != 0 { - if *eipReq.OperatorName == "BGP" { - *eipReq.OperatorName = "Bgp" + RunE: func(cmd *cobra.Command, args []string) error { + *req.Memory *= 1024 + if len(password) > 0 { + req.LoginMode = sdk.String("Password") + req.KeyPairId = nil + req.Password = sdk.String(password) + } else if len(keyPairId) > 0 { + req.LoginMode = sdk.String("KeyPair") + req.KeyPairId = sdk.String(keyPairId) + req.Password = nil + } else { + return fmt.Errorf("password or key-pair-id is required") + } + if len(firewallId) > 0 { + req.SecurityGroupId = sdk.String(firewallId) + } else if len(secGroupIds) > 0 { + if len(secGroupIds) > _MaxBoundSecGroupCount { + return fmt.Errorf("security group count should not be more than 5") } - eipReq.ChargeType = req.ChargeType - eipReq.Tag = req.Tag - eipReq.Quantity = req.Quantity - eipReq.Region = req.Region - eipReq.ProjectId = req.ProjectId - eipResp, err := base.BizClient.AllocateEIP(eipReq) + secGroupList := make([]uhost.CreateUHostInstanceParamSecGroupId, 0) + for idx, secGroupId := range secGroupIds { + secGroupList = append(secGroupList, uhost.CreateUHostInstanceParamSecGroupId{Id: sdk.String(secGroupId), Priority: sdk.Int(1 + idx)}) + } + req.SecGroupId = secGroupList + req.SecurityMode = sdk.String("SecGroup") + } + req.ImageId = sdk.String(base.PickResourceID(*req.ImageId)) + req.VPCId = sdk.String(base.PickResourceID(*req.VPCId)) + req.SubnetId = sdk.String(base.PickResourceID(*req.SubnetId)) + req.IsolationGroup = sdk.String(base.PickResourceID(*req.IsolationGroup)) + if *req.Disks[1].Type == "NONE" || *req.Disks[1].Type == "" { + req.Disks = req.Disks[:1] + } + if hotPlug == "true" || len(userData) > 0 || len(userDataBase64) > 0 { + any, err := describeImageByID(base.PickResourceID(*req.ImageId), *req.ProjectId, *req.Region, *req.Zone) if err != nil { - base.HandleError(err) + return fmt.Errorf("check image support feaures failed: %v", err) } else { - for _, eip := range eipResp.EIPSet { - base.Cxt.Printf("allocate EIP[%s] ", eip.EIPId) - for _, ip := range eip.EIPAddr { - base.Cxt.Printf("IP:%s Line:%s \n", ip.IP, ip.OperatorName) + image, ok := any.(*uhost.UHostImageSet) + if !ok { + return fmt.Errorf("check image support feaures failed, image %s may not exist", *req.ImageId) + } + for _, feature := range image.Features { + if feature == "HotPlug" { + hotPlugImageFlag = true } - if len(resp.UHostIds) == 1 { - bindEIP(sdk.String(resp.UHostIds[0]), sdk.String("uhost"), sdk.String(eip.EIPId), req.ProjectId, req.Region) + if feature == "CloudInit" { + userDataImageFlag = true } } } + if !hotPlugImageFlag && hotPlug == "true" { + base.LogWarn(fmt.Sprintf("warning. image %s does not support hot-plug", *req.ImageId)) + req.HotplugFeature = sdk.Bool(false) + } + + if !userDataImageFlag && (len(userData) > 0 || len(userDataBase64) > 0) { + return fmt.Errorf("image %s does not support user-data feature", *req.ImageId) + } + + if hotPlug == "true" { + req.HotplugFeature = sdk.Bool(true) + } + + if len(userData) > 0 { + req.UserData = sdk.String(base64.StdEncoding.EncodeToString([]byte(userData))) + } + + if len(userDataBase64) > 0 { + req.UserData = sdk.String(userDataBase64) + } + } + if *eipReq.Bandwidth != 0 || *eipReq.PayMode == "ShareBandwidth" { + if *eipReq.OperatorName == "" { + *eipReq.OperatorName = getEIPLine(*req.Region) + } + req.NetworkInterface = []uhost.CreateUHostInstanceParamNetworkInterface{{EIP: &eipReq}} + } + wg := &sync.WaitGroup{} + tokens := make(chan struct{}, concurrent) + wg.Add(count) + batchRename, err := regexp.Match(`\[\d+,\d+\]`, []byte(*req.Name)) + if err != nil || !batchRename { + batchRename = false + } + if batchRename { + var actualRequest uhost.CreateUHostInstanceRequest + actualRequest = *req + if len(bindEipIDs) > 0 { + if len(bindEipIDs) != count { + return fmt.Errorf("bind-eip count should be equal to uhost count") + } + actualRequest.NetworkInterface = nil + } + wg.Add(1 - count) + createMultipleUhostWrapper(&actualRequest, count, updateEIPReq, bindEipIDs, async, make(chan bool, 1), wg, tokens) + + } else if count <= 5 { + for i := 0; i < count; i++ { + bindEipID := "" + if len(bindEipIDs) > i { + bindEipID = bindEipIDs[i] + } + var actualRequest uhost.CreateUHostInstanceRequest + actualRequest = *req + if bindEipID != "" { + actualRequest.NetworkInterface = nil + } + createUhostWrapper(&actualRequest, updateEIPReq, bindEipID, async, make(chan bool, count), wg, tokens, i) + } + } else { + retCh := make(chan bool, count) + ux.Doc.Disable() + refresh := ux.NewRefresh() + + go func(req uhost.CreateUHostInstanceRequest) { + for i := 0; i < count; i++ { + actualRequest := req + bindEipID := "" + if len(bindEipIDs) > i { + bindEipID = bindEipIDs[i] + actualRequest.NetworkInterface = nil + } + go createUhostWrapper(&actualRequest, updateEIPReq, bindEipID, async, retCh, wg, tokens, i) + } + }(*req) + + go func() { + var success, fail int + refresh.Do(fmt.Sprintf("uhost creating, total:%d, success:%d, fail:%d", count, success, fail)) + for ret := range retCh { + if ret { + success++ + } else { + fail++ + } + refresh.Do(fmt.Sprintf("uhost creating, total:%d, success:%d, fail:%d", count, success, fail)) + if count == success+fail && fail > 0 { + fmt.Printf("Check logs in %s\n", base.GetLogFilePath()) + } + } + }() } + wg.Wait() + return nil }, } - n1Zone := map[string]bool{ - "cn-bj2-01": true, - "cn-bj2-03": true, - "cn-sh2-01": true, - "hk-01": true, - } - defaultUhostType := "N2" - if _, ok := n1Zone[base.ConfigInstance.Zone]; ok { - defaultUhostType = "N1" - } - req.Disks = make([]uhost.UHostDisk, 2) req.Disks[0].IsBoot = sdk.String("True") req.Disks[1].IsBoot = sdk.String("False") flags := cmd.Flags() flags.SortFlags = false - async = flags.Bool("async", false, "Optional. Do not wait for the long-running operation to finish.") - req.CPU = flags.Int("cpu", 4, "Required. The count of CPU cores. Optional parameters: {1, 2, 4, 8, 12, 16, 24, 32}") - req.Memory = flags.Int("memory-gb", 8, "Required. Memory size. Unit: GB. Range: [1, 128], multiple of 2") - req.Password = flags.String("password", "", "Required. Password of the uhost user(root/ubuntu)") + req.CPU = flags.Int("cpu", 4, "Required. The count of CPU cores. Optional parameters: {1, 2, 4, 8, 12, 16, 24, 32, 64}") + req.Memory = flags.Int("memory-gb", 8, "Required. Memory size. Unit: GB. Range: [1, 512], multiple of 2") + flags.StringVar(&password, "password", "", "Optional. Password of the uhost user(root/ubuntu)") + flags.StringVar(&keyPairId, "key-pair-id", "", "Optional. Resource ID of ssh key pair. See 'ucloud api --Action DescribeUHostKeyPairs' Where both password and key-pair-id are set, the key-pair-id is ignored") req.ImageId = flags.String("image-id", "", "Required. The ID of image. see 'ucloud image list'") + flags.BoolVar(&async, "async", false, "Optional. Do not wait for the long-running operation to finish.") + flags.IntVar(&count, "count", 1, "Optional. Number of uhost to create.") + flags.IntVar(&concurrent, "concurrent", 20, "Optional. The count of concurrent uhost creation requests.") req.VPCId = flags.String("vpc-id", "", "Optional. VPC ID. This field is required under VPC2.0. See 'ucloud vpc list'") req.SubnetId = flags.String("subnet-id", "", "Optional. Subnet ID. This field is required under VPC2.0. See 'ucloud subnet list'") req.Name = flags.String("name", "UHost", "Optional. UHost instance name") - bindEipID = flags.String("bind-eip", "", "Optional. Bind eip to uhost. Value could be resource id or IP Address") - eipReq.OperatorName = flags.String("create-eip-line", "", "Optional. Required if you want to create new EIP. Line of created eip to bind with the uhost") - eipReq.Bandwidth = cmd.Flags().Int("create-eip-bandwidth-mb", 0, "Optional. Required if you want to create new EIP. Bandwidth(Unit:Mbps).The range of value related to network charge mode. By traffic [1, 200]; by bandwidth [1,800] (Unit: Mbps); it could be 0 if the eip belong to the shared bandwidth") - eipReq.PayMode = cmd.Flags().String("create-eip-charge-mode", "Bandwidth", "Optional. 'Traffic','Bandwidth' or 'ShareBandwidth'") - eipReq.Name = flags.String("create-eip-name", "", "Optional. Name of created eip to bind with the uhost") - eipReq.Remark = cmd.Flags().String("create-eip-remark", "", "Optional.Remark of your EIP.") - eipReq.CouponId = cmd.Flags().String("create-eip-coupon-id", "", "Optional.Coupon ID, The Coupon can deducte part of the payment,see https://accountv2.ucloud.cn") - - req.ChargeType = flags.String("charge-type", "Month", "Optional.'Year',pay yearly;'Month',pay monthly;'Dynamic', pay hourly(requires access)") + flags.StringSliceVar(&bindEipIDs, "bind-eip", nil, "Optional. Resource ID or IP Address of eip that will be bound to the new created uhost") + eipReq.OperatorName = flags.String("create-eip-line", "", "Optional. BGP for regions in the chinese mainland and International for overseas regions") + eipReq.Bandwidth = flags.Int("create-eip-bandwidth-mb", 0, "Optional. Required if you want to create new EIP. Bandwidth(Unit:Mbps).The range of value related to network charge mode. By traffic [1, 300]; by bandwidth [1,800] (Unit: Mbps); it could be 0 if the eip belong to the shared bandwidth") + eipReq.PayMode = flags.String("create-eip-traffic-mode", "Bandwidth", "Optional. 'Traffic','Bandwidth' or 'ShareBandwidth'") + eipReq.ShareBandwidthId = flags.String("shared-bw-id", "", "Optional. Resource ID of shared bandwidth. It takes effect when create-eip-traffic-mode is ShareBandwidth ") + updateEIPReq.Name = flags.String("create-eip-name", "", "Optional. Name of created eip to bind with the uhost") + updateEIPReq.Remark = flags.String("create-eip-remark", "", "Optional.Remark of your EIP.") + + req.ChargeType = flags.String("charge-type", "Month", "Optional.'Year',pay yearly;'Month',pay monthly;'Dynamic', pay hourly") req.Quantity = flags.Int("quantity", 1, "Optional. The duration of the instance. N years/months.") - req.ProjectId = flags.String("project-id", base.ConfigInstance.ProjectID, "Optional. Assign project-id") - req.Region = flags.String("region", base.ConfigInstance.Region, "Optional. Assign region") - req.Zone = flags.String("zone", base.ConfigInstance.Zone, "Optional. Assign availability zone") - req.UHostType = flags.String("type", defaultUhostType, "Optional. Default is 'N2' of which cpu is V4 and sata disk. also support 'N1' means V3 cpu and sata disk;'I2' means V4 cpu and ssd disk;'D1' means big data model;'G1' means GPU type, model for K80;'G2' model for P40; 'G3' model for V100") - req.NetCapability = flags.String("net-capability", "Normal", "Optional. Default is 'Normal', also support 'Super' which will enhance multiple times network capability as before") - req.Disks[0].Type = flags.String("os-disk-type", "LOCAL_NORMAL", "Optional. Enumeration value. 'LOCAL_NORMAL', Ordinary local disk; 'CLOUD_NORMAL', Ordinary cloud disk; 'LOCAL_SSD',local ssd disk; 'CLOUD_SSD',cloud ssd disk; 'EXCLUSIVE_LOCAL_DISK',big data. The disk only supports a limited combination.") + bindProjectID(req, flags) + bindRegion(req, flags) + bindZone(req, flags) + + req.MachineType = flags.String("machine-type", "N", "Optional. Accept values: N, C, G, O, OS. Forward to https://docs.ucloud.cn/api/uhost-api/uhost_type for details") + req.MinimalCpuPlatform = flags.String("minimal-cpu-platform", "", "Optional. Accept values: Intel/Auto, Intel/IvyBridge, Intel/Haswell, Intel/Broadwell, Intel/Skylake, Intel/Cascadelake") + req.UHostType = flags.String("type", "", "Optional. Accept values: N1, N2, N3, G1, G2, G3, I1, I2, C1. Forward to https://docs.ucloud.cn/api/uhost-api/uhost_type for details") + req.GPU = flags.Int("gpu", 0, "Optional. The count of GPU cores.") + req.NetCapability = flags.String("net-capability", "Normal", "Optional. Accept values: Normal, Super and Ultra. 'Normal' will disable network enhancement. 'Super' will enable network enhancement 1.0. 'Ultra' will enable network enhancement 2.0") + flags.StringVar(&hotPlug, "hot-plug", "true", "Optional. Enable hot plug feature or not. Accept values: true or false") + req.Disks[0].Type = flags.String("os-disk-type", "CLOUD_SSD", "Optional. Enumeration value. 'LOCAL_NORMAL', Ordinary local disk; 'CLOUD_NORMAL', Ordinary cloud disk; 'LOCAL_SSD',local ssd disk; 'CLOUD_SSD',cloud ssd disk; 'EXCLUSIVE_LOCAL_DISK',big data. The disk only supports a limited combination.") req.Disks[0].Size = flags.Int("os-disk-size-gb", 20, "Optional. Default 20G. Windows should be bigger than 40G Unit GB") req.Disks[0].BackupType = flags.String("os-disk-backup-type", "NONE", "Optional. Enumeration value, 'NONE' or 'DATAARK'. DataArk supports real-time backup, which can restore the disk back to any moment within the last 12 hours. (Normal Local Disk and Normal Cloud Disk Only)") - req.Disks[1].Type = flags.String("data-disk-type", "LOCAL_NORMAL", "Optional. Enumeration value. 'LOCAL_NORMAL', Ordinary local disk; 'CLOUD_NORMAL', Ordinary cloud disk; 'LOCAL_SSD',local ssd disk; 'CLOUD_SSD',cloud ssd disk; 'EXCLUSIVE_LOCAL_DISK',big data. The disk only supports a limited combination.") + req.Disks[1].Type = flags.String("data-disk-type", "CLOUD_SSD", "Optional. Accept values: 'LOCAL_NORMAL','LOCAL_SSD','CLOUD_NORMAL',CLOUD_SSD','CLOUD_RSSD','EXCLUSIVE_LOCAL_DISK' and 'NONE'. 'LOCAL_NORMAL', Ordinary local disk; 'CLOUD_NORMAL', Ordinary cloud disk; 'LOCAL_SSD',local ssd disk; 'CLOUD_SSD',cloud ssd disk; 'CLOUD_RSSD', coud rssd disk; 'EXCLUSIVE_LOCAL_DISK',big data. The disk only supports a limited combination. 'NONE', create uhost without data disk. More details https://docs.ucloud.cn/api/uhost-api/disk_type") req.Disks[1].Size = flags.Int("data-disk-size-gb", 20, "Optional. Disk size. Unit GB") req.Disks[1].BackupType = flags.String("data-disk-backup-type", "NONE", "Optional. Enumeration value, 'NONE' or 'DATAARK'. DataArk supports real-time backup, which can restore the disk back to any moment within the last 12 hours. (Normal Local Disk and Normal Cloud Disk Only)") - req.NetworkId = flags.String("network-id", "", "Optional. Network ID (no need to fill in the case of VPC2.0). In the case of VPC1.0, if not filled in, we will choose the basic network; if it is filled in, we will choose the subnet. See 'ucloud subnet list'.") - req.SecurityGroupId = flags.String("firewall-id", "", "Optional. Firewall Id, default: Web recommended firewall. see 'ucloud firewall list'.") + flags.StringVar(&firewallId, "firewall-id", "", "Optional. Firewall Id, default: Web recommended firewall. see 'ucloud firewall list'.") + flags.StringSliceVar(&secGroupIds, "security-group-id", nil, "Optional. Security Group Id. Before using security group function, please confirm the account has such permission. When both firewall-id and security-group-id are set, the security-group-id will be ignored") req.Tag = flags.String("group", "Default", "Optional. Business group") - req.CouponId = flags.String("coupon-id", "", "Optional. Coupon ID, The Coupon can deduct part of the payment,see https://accountv2.ucloud.cn") - - cmd.Flags().SetFlagValues("charge-type", "Month", "Year", "Dynamic", "Trial") - cmd.Flags().SetFlagValues("cpu", "1", "2", "4", "8", "12", "16", "24", "32") - cmd.Flags().SetFlagValues("type", "N2", "N1", "I2", "D1", "G1", "G2", "G3") - cmd.Flags().SetFlagValues("net-capability", "Normal", "Super") - cmd.Flags().SetFlagValues("os-disk-type", "LOCAL_NORMAL", "CLOUD_NORMAL", "LOCAL_SSD", "CLOUD_SSD", "EXCLUSIVE_LOCAL_DISK") - cmd.Flags().SetFlagValues("os-disk-backup-type", "NONE", "DATAARK") - cmd.Flags().SetFlagValues("data-disk-type", "LOCAL_NORMAL", "CLOUD_NORMAL", "LOCAL_SSD", "CLOUD_SSD", "EXCLUSIVE_LOCAL_DISK") - cmd.Flags().SetFlagValues("data-disk-backup-type", "NONE", "DATAARK") - cmd.Flags().SetFlagValues("create-eip-line", "BGP", "International") - cmd.Flags().SetFlagValues("create-eip-charge-mode", "Bandwidth", "Traffic", "ShareBandwidth") - - cmd.Flags().SetFlagValuesFunc("image-id", func() []string { - req := base.BizClient.NewDescribeImageRequest() - projectID, _ := flags.GetString("project-id") - if projectID == "" { - projectID = base.ConfigInstance.ProjectID + req.IsolationGroup = flags.String("isolation-group", "", "Optional. Resource ID of isolation group. see 'ucloud uhost isolation-group list") + req.GpuType = flags.String("gpu-type", "", "Optional. The type of GPU instance. Required if defined the `machine-type` as 'G'. Accept values: 'K80', 'P40', 'V100'. Forward to https://docs.ucloud.cn/api/uhost-api/uhost_type for details.") + flags.StringVar(&userData, "user-data", "", "Optional. Conflicts with `user-data-base64`. ConCustomize the startup behaviors when launching the instance. Forward to https://docs.ucloud.cn/uhost/guide/metadata/userdata for details.") + flags.StringVar(&userDataBase64, "user-data-base64", "", "Optional. Conflicts with `user-data`. Customize the startup behaviors when launching the instance. The value must be base64-encode. Forward to https://docs.ucloud.cn/uhost/guide/metadata/userdata for details.") + + flags.MarkDeprecated("type", "please use --machine-type instead") + flags.SetFlagValues("charge-type", "Month", "Year", "Dynamic", "Trial") + flags.SetFlagValues("hot-plug", "true", "false") + flags.SetFlagValues("cpu", "1", "2", "4", "8", "12", "16", "24", "32", "64") + flags.SetFlagValues("type", "N2", "N1", "N3", "I2", "I1", "C1", "G1", "G2", "G3") + flags.SetFlagValues("machine-type", "N", "C", "G", "O", "OS") + flags.SetFlagValues("minimal-cpu-platform", "Intel/Auto", "Intel/IvyBridge", "Intel/Haswell", "Intel/Broadwell", "Intel/Skylake", "Intel/Cascadelake") + flags.SetFlagValues("net-capability", "Normal", "Super", "Ultra") + flags.SetFlagValues("os-disk-type", "LOCAL_NORMAL", "CLOUD_NORMAL", "LOCAL_SSD", "CLOUD_SSD", "CLOUD_RSSD", "EXCLUSIVE_LOCAL_DISK") + flags.SetFlagValues("os-disk-backup-type", "NONE", "DATAARK") + flags.SetFlagValues("data-disk-type", "LOCAL_NORMAL", "CLOUD_NORMAL", "LOCAL_SSD", "CLOUD_SSD", "CLOUD_RSSD", "EXCLUSIVE_LOCAL_DISK", "NONE") + flags.SetFlagValues("data-disk-backup-type", "NONE", "DATAARK") + flags.SetFlagValues("create-eip-line", "BGP", "International") + flags.SetFlagValues("create-eip-traffic-mode", "Bandwidth", "Traffic", "ShareBandwidth") + flags.SetFlagValues("gpu-type", "K80", "P40", "V100") + + flags.SetFlagValuesFunc("image-id", func() []string { + return getImageList([]string{status.IMAGE_AVAILABLE}, cli.IMAGE_BASE, *req.ProjectId, *req.Region, *req.Zone) + }) + flags.SetFlagValuesFunc("vpc-id", func() []string { + return getAllVPCIdNames(*req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("bind-eip", func() []string { + return getAllEip(*req.ProjectId, *req.Region, []string{status.EIP_FREE}, nil) + }) + flags.SetFlagValuesFunc("firewall-id", func() []string { + return getFirewallIDNames(*req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("subnet-id", func() []string { + return getAllSubnetIDNames(*req.VPCId, *req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("isolation-group", func() []string { + return getIsolationGroupList(*req.ProjectId, *req.Region) + }) + + cmd.MarkFlagRequired("cpu") + cmd.MarkFlagRequired("memory-gb") + cmd.MarkFlagRequired("image-id") + + return cmd +} + +// createMultipleUhostWrapper 处理UI和并发控制 +func createMultipleUhostWrapper(req *uhost.CreateUHostInstanceRequest, count int, updateEIPReq *unet.UpdateEIPAttributeRequest, bindEipIDs []string, async bool, retCh chan<- bool, wg *sync.WaitGroup, tokens chan struct{}) { + //控制并发数量 + tokens <- struct{}{} + defer func() { + <-tokens + //设置延时,使报错能渲染出来 + time.Sleep(time.Second / 5) + wg.Done() + }() + + success, logs := createMultipleUhost(req, count, updateEIPReq, bindEipIDs, async) + retCh <- success + logs = append(logs, fmt.Sprintf("result:%t", success)) + base.LogInfo(logs...) +} + +// createUhostWrapper 处理UI和并发控制 +func createUhostWrapper(req *uhost.CreateUHostInstanceRequest, updateEIPReq *unet.UpdateEIPAttributeRequest, bindEipID string, async bool, retCh chan<- bool, wg *sync.WaitGroup, tokens chan struct{}, idx int) { + //控制并发数量 + tokens <- struct{}{} + defer func() { + <-tokens + //设置延时,使报错能渲染出来 + time.Sleep(time.Second / 5) + wg.Done() + }() + + success, logs := createUhost(req, updateEIPReq, bindEipID, async) + retCh <- success + logs = append(logs, fmt.Sprintf("index:%d, result:%t", idx, success)) + base.LogInfo(logs...) +} + +func createMultipleUhost(req *uhost.CreateUHostInstanceRequest, count int, updateEIPReq *unet.UpdateEIPAttributeRequest, bindEipIDs []string, async bool) (bool, []string) { + if req.MaxCount == nil { + req.MaxCount = sdk.Int(1) + } + req.MaxCount = sdk.Int(count) + + resp, err := base.BizClient.CreateUHostInstance(req) + block := ux.NewBlock() + ux.Doc.Append(block) + logs := []string{"=================================================="} + logs = append(logs, fmt.Sprintf("api:CreateUHostInstance, request:%v", base.ToQueryMap(req))) + if err != nil { + logs = append(logs, fmt.Sprintf("err:%v", err)) + block.Append(base.ParseError(err)) + return false, logs + } + if len(bindEipIDs) > 0 && len(bindEipIDs) != count { + block.Append(fmt.Sprintf("expect eip count %d, accept %d", count, len(bindEipIDs))) + return false, logs + } + + logs = append(logs, fmt.Sprintf("resp:%#v", resp)) + + if len(resp.UHostIds) != *req.MaxCount { + block.Append(fmt.Sprintf("expect uhost count %d, accept %d", count, len(resp.UHostIds))) + return false, logs + } + for i, uhostID := range resp.UHostIds { + block = ux.NewBlock() + ux.Doc.Append(block) + + text := fmt.Sprintf("the uhost[%s]", uhostID) + if len(req.Disks) > 1 { + text = fmt.Sprintf("%s which attached a data disk", text) + if len(req.NetworkInterface) > 0 { + text = fmt.Sprintf("%s and binded an eip", text) + } + } else if len(req.NetworkInterface) > 0 { + text = fmt.Sprintf("%s which binded an eip", text) + } + text = fmt.Sprintf("%s is initializing", text) + + if async { + block.Append(text) + } else { + uhostSpoller.Sspoll(uhostID, text, []string{status.HOST_RUNNING, status.HOST_FAIL}, block, &req.CommonBase) + } + bindEipID := "" + if len(bindEipIDs) > i { + bindEipID = bindEipIDs[i] + } + + if bindEipID != "" { + eip := base.PickResourceID(bindEipID) + logs = append(logs, fmt.Sprintf("bind eip: %s", eip)) + eipLogs, err := sbindEIP(sdk.String(uhostID), sdk.String("uhost"), &eip, req.ProjectId, req.Region) + logs = append(logs, eipLogs...) + if err != nil { + block.Append(fmt.Sprintf("bind eip[%s] with uhost[%s] failed: %v", eip, uhostID, err)) + return false, logs + } + block.Append(fmt.Sprintf("bind eip[%s] with uhost[%s] successfully", eip, uhostID)) + } else if len(req.NetworkInterface) > 0 { + ipSet, err := getEIPByUHostId(uhostID) + if err != nil { + block.Append(err.Error()) + return false, logs + } + block.Append(fmt.Sprintf("IP:%s Line:%s", ipSet.IP, ipSet.Type)) + if *updateEIPReq.Name != "" || *updateEIPReq.Remark != "" { + var message string + if *updateEIPReq.Name != "" && *updateEIPReq.Remark != "" { + message = "name and remark" + } else if *updateEIPReq.Name != "" { + message = "name" + } else { + message = "remark" + } + + logs = append(logs, fmt.Sprintf("update attribute %s of eip[%s] binded uhost[%s]", message, ipSet.IPId, uhostID)) + updateEIPReq.EIPId = sdk.String(ipSet.IPId) + _, err = base.BizClient.UpdateEIPAttribute(updateEIPReq) + if err != nil { + block.Append(fmt.Sprintf("update attribute %s of eip[%s] binded uhost[%s] got err, %s", message, ipSet.IPId, uhostID, err)) + return false, logs + } + block.Append(fmt.Sprintf("update attribute %s of eip[%s] binded uhost[%s] successfully", message, ipSet.IPId, uhostID)) + } + } + } + return true, logs +} + +func createUhost(req *uhost.CreateUHostInstanceRequest, updateEIPReq *unet.UpdateEIPAttributeRequest, bindEipID string, async bool) (bool, []string) { + resp, err := base.BizClient.CreateUHostInstance(req) + block := ux.NewBlock() + ux.Doc.Append(block) + logs := []string{"=================================================="} + logs = append(logs, fmt.Sprintf("api:CreateUHostInstance, request:%v", base.ToQueryMap(req))) + if err != nil { + logs = append(logs, fmt.Sprintf("err:%v", err)) + block.Append(base.ParseError(err)) + return false, logs + } + + logs = append(logs, fmt.Sprintf("resp:%#v", resp)) + if len(resp.UHostIds) != 1 { + block.Append(fmt.Sprintf("expect uhost count 1 , accept %d", len(resp.UHostIds))) + return false, logs + } + + text := fmt.Sprintf("the uhost[%s]", resp.UHostIds[0]) + if len(req.Disks) > 1 { + text = fmt.Sprintf("%s which attached a data disk", text) + if len(req.NetworkInterface) > 0 { + text = fmt.Sprintf("%s and binded an eip", text) + } + } else if len(req.NetworkInterface) > 0 { + text = fmt.Sprintf("%s which binded an eip", text) + } + text = fmt.Sprintf("%s is initializing", text) + + if async { + block.Append(text) + } else { + uhostSpoller.Sspoll(resp.UHostIds[0], text, []string{status.HOST_RUNNING, status.HOST_FAIL}, block, &req.CommonBase) + } + + if bindEipID != "" { + eip := base.PickResourceID(bindEipID) + logs = append(logs, fmt.Sprintf("bind eip: %s", eip)) + eipLogs, err := sbindEIP(sdk.String(resp.UHostIds[0]), sdk.String("uhost"), &eip, req.ProjectId, req.Region) + logs = append(logs, eipLogs...) + if err != nil { + block.Append(fmt.Sprintf("bind eip[%s] with uhost[%s] failed: %v", eip, resp.UHostIds[0], err)) + return false, logs } - req.ProjectId = sdk.String(projectID) + block.Append(fmt.Sprintf("bind eip[%s] with uhost[%s] successfully", eip, resp.UHostIds[0])) + } else if len(req.NetworkInterface) > 0 { + ipSet, err := getEIPByUHostId(resp.UHostIds[0]) + if err != nil { + block.Append(err.Error()) + return false, logs + } + block.Append(fmt.Sprintf("IP:%s Line:%s", ipSet.IP, ipSet.Type)) + if *updateEIPReq.Name != "" || *updateEIPReq.Remark != "" { + var message string + if *updateEIPReq.Name != "" && *updateEIPReq.Remark != "" { + message = "name and remark" + } else if *updateEIPReq.Name != "" { + message = "name" + } else { + message = "remark" + } - region, _ := flags.GetString("region") - if region == "" { - region = base.ConfigInstance.Region + logs = append(logs, fmt.Sprintf("update attribute %s of eip[%s] binded uhost[%s]", message, ipSet.IPId, resp.UHostIds[0])) + updateEIPReq.EIPId = sdk.String(ipSet.IPId) + _, err = base.BizClient.UpdateEIPAttribute(updateEIPReq) + if err != nil { + block.Append(fmt.Sprintf("update attribute %s of eip[%s] binded uhost[%s] got err, %s", message, ipSet.IPId, resp.UHostIds[0], err)) + return false, logs + } + block.Append(fmt.Sprintf("update attribute %s of eip[%s] binded uhost[%s] successfully", message, ipSet.IPId, resp.UHostIds[0])) } - req.Region = sdk.String(region) + } + return true, logs +} + +func getEIPByUHostId(uhostId string) (*uhost.UHostIPSet, error) { + if uhostId == "" { + return nil, fmt.Errorf("the uhost[%s] is not found", uhostId) + } + for i := 0; i <= 5; i++ { + req := base.BizClient.NewDescribeUHostInstanceRequest() + req.UHostIds = []string{uhostId} - zone, _ := flags.GetString("zone") - if zone == "" { - zone = base.ConfigInstance.Zone + resp, err := base.BizClient.DescribeUHostInstance(req) + if err != nil { + return nil, err + } + if len(resp.UHostSet) < 1 { + return nil, fmt.Errorf("the uhost[%s] is not found", uhostId) } - req.Zone = sdk.String(zone) - req.ImageType = sdk.String("Base") - req.Limit = sdk.Int(1000) - result := make([]string, 0) - resp, err := base.BizClient.DescribeImage(req) - if err == nil { - for _, image := range resp.ImageSet { - if image.State == "Available" { - imageName := strings.Replace(image.ImageName, " ", "-", -1) - result = append(result, fmt.Sprintf("%s/%s", image.ImageId, imageName)) + + if len(resp.UHostSet[0].IPSet) > 0 { + for _, v := range resp.UHostSet[0].IPSet { + if v.Type != "Private" && v.IPId != "" { + return &v, nil } } } - return result - }) - cmd.MarkFlagRequired("cpu") - cmd.MarkFlagRequired("memory") - cmd.MarkFlagRequired("password") - cmd.MarkFlagRequired("image-id") + time.Sleep(1 * time.Second) + } - return cmd + return nil, fmt.Errorf("can not get eip by uhost[%s]", uhostId) } -//NewCmdUHostDelete ucloud uhost delete +// NewCmdUHostDelete ucloud uhost delete func NewCmdUHostDelete() *cobra.Command { var uhostIDs *[]string - var isDestory = sdk.Bool(false) + var isDestroy = sdk.Bool(false) var yes *bool - + var releaseEIP bool + var releaseUDisk bool req := base.BizClient.NewTerminateUHostInstanceRequest() cmd := &cobra.Command{ Use: "delete", @@ -319,7 +821,7 @@ func NewCmdUHostDelete() *cobra.Command { Long: "Delete Uhost instance", Run: func(cmd *cobra.Command, args []string) { if !*yes { - sure, err := ux.Prompt("Are you sure you want to delete this host?") + sure, err := ux.Prompt("Are you sure you want to delete the host(s)?") if err != nil { base.Cxt.Println(err) return @@ -328,59 +830,87 @@ func NewCmdUHostDelete() *cobra.Command { return } } - if *isDestory { + if *isDestroy { req.Destroy = sdk.Int(1) } else { req.Destroy = sdk.Int(0) } - for _, id := range *uhostIDs { + req.ReleaseEIP = &releaseEIP + req.ReleaseUDisk = &releaseUDisk + reqs := make([]request.Common, len(*uhostIDs)) + for idx, id := range *uhostIDs { + _req := *req id = base.PickResourceID(id) - req.UHostId = &id - hostIns, err := describeUHostByID(*req.UHostId, *req.ProjectId, *req.Region, *req.Zone) - if err != nil { - base.HandleError(err) - } else if hostIns != nil { - ins := hostIns.(*uhost.UHostInstanceSet) - if ins.State == "Running" { - _req := base.BizClient.NewStopUHostInstanceRequest() - _req.ProjectId = req.ProjectId - _req.Region = req.Region - _req.Zone = req.Zone - _req.UHostId = req.UHostId - stopUhostIns(_req, false) - } - } - resp, err := base.BizClient.TerminateUHostInstance(req) - if err != nil { - base.HandleError(err) - } else { - base.Cxt.Printf("UHost:[%v] deleted\n", resp.UHostId) - } + _req.UHostId = sdk.String(id) + reqs[idx] = &_req } + coAction := newConcurrentAction(reqs, 50, deleteUHost) + coAction.Do() }, } - cmd.Flags().SortFlags = false - uhostIDs = cmd.Flags().StringSlice("resource-id", nil, "Requried. ResourceIDs(UhostIds) of the uhost instance") - req.ProjectId = cmd.Flags().String("project-id", base.ConfigInstance.ProjectID, "Optional. Assign project-id") - req.Region = cmd.Flags().String("region", base.ConfigInstance.Region, "Optional. Assign region") + flags := cmd.Flags() + flags.SortFlags = false + + uhostIDs = cmd.Flags().StringSlice("uhost-id", nil, "Requried. ResourceIDs(UhostIds) of the uhost instance") + bindRegion(req, flags) + bindProjectID(req, flags) req.Zone = cmd.Flags().String("zone", "", "Optional. availability zone") - isDestory = cmd.Flags().Bool("destory", false, "Optional. false,the uhost instance will be thrown to UHost recycle If you have permission; true,the uhost instance will be deleted directly") - req.ReleaseEIP = cmd.Flags().Bool("release-eip", false, "Optional. false,Unbind EIP only; true, Unbind EIP and release it") - req.ReleaseUDisk = cmd.Flags().Bool("delete-cloud-disk", false, "Optional.false,Detach cloud disk only; true, Detach cloud disk and delete it") + isDestroy = cmd.Flags().Bool("destroy", false, "Optional. false,the uhost instance will be thrown to UHost recycle if you have permission; true,the uhost instance will be deleted directly") + cmd.Flags().BoolVar(&releaseEIP, "release-eip", true, "Optional. false,Unbind EIP only; true, Unbind EIP and release it") + cmd.Flags().BoolVar(&releaseUDisk, "delete-cloud-disk", true, "Optional. false, detach cloud disk only; true, detach cloud disk and delete it") yes = cmd.Flags().BoolP("yes", "y", false, "Optional. Do not prompt for confirmation.") - cmd.Flags().SetFlagValues("destory", "true", "false") + cmd.Flags().SetFlagValues("destroy", "true", "false") cmd.Flags().SetFlagValues("release-eip", "true", "false") cmd.Flags().SetFlagValues("delete-cloud-disk", "true", "false") - cmd.Flags().SetFlagValuesFunc("resource-id", func() []string { - return getUhostList([]string{status.HOST_RUNNING, status.HOST_FAIL, status.HOST_FAIL}, *req.ProjectId, *req.Region, *req.Zone) + cmd.Flags().SetFlagValuesFunc("uhost-id", func() []string { + return getUhostList([]string{status.HOST_RUNNING, status.HOST_STOPPED, status.HOST_FAIL}, *req.ProjectId, *req.Region, *req.Zone) }) - cmd.MarkFlagRequired("resource-id") + cmd.MarkFlagRequired("uhost-id") return cmd } -//NewCmdUHostStop ucloud uhost stop -func NewCmdUHostStop() *cobra.Command { +func deleteUHost(creq request.Common) (bool, []string) { + req := creq.(*uhost.TerminateUHostInstanceRequest) + block := ux.NewBlock() + ux.Doc.Append(block) + logs := []string{} + hostIns, err := sdescribeUHostByID(*req.UHostId, nil) + if err != nil { + logs = append(logs, fmt.Sprintf("describe uhost[%s] failed: %s", *req.UHostId, base.ParseError(err))) + return false, logs + } + + if hostIns == nil { + logs = append(logs, fmt.Sprintf("uhost[%s] does not exist", *req.UHostId)) + return false, logs + } + + ins := hostIns.(*uhost.UHostInstanceSet) + if ins.State == "Running" { + _req := base.BizClient.NewStopUHostInstanceRequest() + _req.ProjectId = req.ProjectId + _req.Region = req.Region + _req.Zone = req.Zone + _req.UHostId = req.UHostId + stopUhostInsV2(_req, false, block) + } + + logs = append(logs, fmt.Sprintf("api:TerminateUHostInstance, request:%v", base.ToQueryMap(req))) + resp, err := base.BizClient.TerminateUHostInstance(req) + if err != nil { + block.Append(base.ParseError(err)) + logs = append(logs, fmt.Sprintf("delete uhost[%s] failed: %s", *req.UHostId, base.ParseError(err))) + return false, logs + } + text := fmt.Sprintf("uhost[%s] deleted", resp.UHostId) + logs = append(logs, text) + block.Append(text) + return true, logs +} + +// NewCmdUHostStop ucloud uhost stop +func NewCmdUHostStop(out io.Writer) *cobra.Command { var uhostIDs *[]string var async *bool req := base.BizClient.NewStopUHostInstanceRequest() @@ -388,48 +918,77 @@ func NewCmdUHostStop() *cobra.Command { Use: "stop", Short: "Shut down uhost instance", Long: "Shut down uhost instance", - Example: "ucloud uhost stop --resource-id uhost-xxx1,uhost-xxx2", + Example: "ucloud uhost stop --uhost-id uhost-xxx1,uhost-xxx2", Run: func(cmd *cobra.Command, args []string) { for _, id := range *uhostIDs { id = base.PickResourceID(id) req.UHostId = &id - stopUhostIns(req, *async) + stopUhostIns(req, *async, out) } }, } cmd.Flags().SortFlags = false - uhostIDs = cmd.Flags().StringSlice("resource-id", nil, "Required. ResourceIDs(UHostIds) of the uhost instances") - req.ProjectId = cmd.Flags().String("project-id", base.ConfigInstance.ProjectID, "Optional. Assign project-id") - req.Region = cmd.Flags().String("region", base.ConfigInstance.Region, "Optional. Assign region") + uhostIDs = cmd.Flags().StringSlice("uhost-id", nil, "Required. ResourceIDs(UHostIds) of the uhost instances") + req.ProjectId = cmd.Flags().String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + req.Region = cmd.Flags().String("region", base.ConfigIns.Region, "Optional. Assign region") req.Zone = cmd.Flags().String("zone", "", "Optional. Assign availability zone") async = cmd.Flags().Bool("async", false, "Optional. Do not wait for the long-running operation to finish.") - cmd.Flags().SetFlagValuesFunc("resource-id", func() []string { + cmd.Flags().SetFlagValuesFunc("uhost-id", func() []string { return getUhostList([]string{status.HOST_RUNNING}, *req.ProjectId, *req.Region, *req.Zone) }) - cmd.MarkFlagRequired("resource-id") + cmd.MarkFlagRequired("uhost-id") return cmd } -func stopUhostIns(req *uhost.StopUHostInstanceRequest, async bool) { +func promptStopUhostIns(req *uhost.StopUHostInstanceRequest, yes, async bool, promptText string, out io.Writer) bool { + if !yes { + agreeClose, err := ux.Prompt(promptText) + if err != nil { + base.LogError(err.Error()) + return false + } + if !agreeClose { + return false + } + } + return stopUhostIns(req, false, out) +} + +func stopUhostIns(req *uhost.StopUHostInstanceRequest, async bool, out io.Writer) bool { resp, err := base.BizClient.StopUHostInstance(req) if err != nil { base.HandleError(err) + return false + } + + text := fmt.Sprintf("uhost[%v] is shutting down", resp.UHostId) + if async { + fmt.Fprintln(out, text) + return false + } + poller := base.NewPoller(describeUHostByID, out) + return poller.Poll(resp.UHostId, *req.ProjectId, *req.Region, *req.Zone, text, []string{status.HOST_STOPPED, status.HOST_FAIL}) +} + +// 可并发调用版本 +func stopUhostInsV2(req *uhost.StopUHostInstanceRequest, async bool, block *ux.Block) { + resp, err := base.BizClient.StopUHostInstance(req) + if err != nil { + block.Append(base.ParseError(err)) + return + } + + text := fmt.Sprintf("uhost[%v] is shutting down", resp.UHostId) + if async { + block.Append(text) } else { - text := fmt.Sprintf("UHost:[%v] is shutting down", resp.UhostId) - if async { - base.Cxt.Println(text) - } else { - done := pollUhost(resp.UhostId, *req.ProjectId, *req.Region, *req.Zone, []string{status.HOST_STOPPED, status.HOST_FAIL}) - ux.DotSpinner.Start(text) - <-done - ux.DotSpinner.Stop() - } + uhostSpoller.Sspoll(resp.UHostId, text, []string{status.HOST_STOPPED, status.HOST_FAIL}, block, nil) } } -//NewCmdUHostStart ucloud uhost start -func NewCmdUHostStart() *cobra.Command { +// NewCmdUHostStart ucloud uhost start +func NewCmdUHostStart(out io.Writer) *cobra.Command { var async *bool var uhostIDs *[]string req := base.BizClient.NewStartUHostInstanceRequest() @@ -437,7 +996,7 @@ func NewCmdUHostStart() *cobra.Command { Use: "start", Short: "Start Uhost instance", Long: "Start Uhost instance", - Example: "ucloud uhost start --resource-id uhost-xxx1,uhost-xxx2", + Example: "ucloud uhost start --uhost-id uhost-xxx1,uhost-xxx2", Run: func(cmd *cobra.Command, args []string) { for _, id := range *uhostIDs { id := base.PickResourceID(id) @@ -446,36 +1005,32 @@ func NewCmdUHostStart() *cobra.Command { if err != nil { base.HandleError(err) } else { - text := fmt.Sprintf("UHost:[%v] is starting", resp.UhostId) + text := fmt.Sprintf("uhost[%v] is starting", resp.UHostId) if *async { - base.Cxt.Println(text) + fmt.Fprintln(out, text) } else { - done := pollUhost(resp.UhostId, *req.ProjectId, *req.Region, *req.Zone, []string{status.HOST_RUNNING, status.HOST_FAIL}) - dotSpinner := ux.NewDotSpinner() - dotSpinner.Start(text) - <-done - dotSpinner.Stop() + poller := base.NewPoller(describeUHostByID, out) + poller.Poll(resp.UHostId, *req.ProjectId, *req.Region, *req.Zone, text, []string{status.HOST_RUNNING, status.HOST_FAIL}) } } } }, } cmd.Flags().SortFlags = false - uhostIDs = cmd.Flags().StringSlice("resource-id", nil, "Requried. ResourceIDs(UHostIds) of the uhost instance") - req.ProjectId = cmd.Flags().String("project-id", base.ConfigInstance.ProjectID, "Optional. Assign project-id") - req.Region = cmd.Flags().String("region", base.ConfigInstance.Region, "Optional. Assign region") + uhostIDs = cmd.Flags().StringSlice("uhost-id", nil, "Requried. ResourceIDs(UHostIds) of the uhost instance") + req.ProjectId = cmd.Flags().String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + req.Region = cmd.Flags().String("region", base.ConfigIns.Region, "Optional. Assign region") req.Zone = cmd.Flags().String("zone", "", "Optional. Assign availability zone") - req.DiskPassword = cmd.Flags().String("disk-password", "", "Optional. Encrypted disk password") async = cmd.Flags().Bool("async", false, "Optional. Do not wait for the long-running operation to finish.") - cmd.Flags().SetFlagValuesFunc("resource-id", func() []string { + cmd.Flags().SetFlagValuesFunc("uhost-id", func() []string { return getUhostList([]string{status.HOST_STOPPED}, *req.ProjectId, *req.Region, *req.Zone) }) - cmd.MarkFlagRequired("resource-id") + cmd.MarkFlagRequired("uhost-id") return cmd } -//NewCmdUHostReboot ucloud uhost restart -func NewCmdUHostReboot() *cobra.Command { +// NewCmdUHostReboot ucloud uhost restart +func NewCmdUHostReboot(out io.Writer) *cobra.Command { var uhostIDs *[]string var async *bool req := base.BizClient.NewRebootUHostInstanceRequest() @@ -483,7 +1038,7 @@ func NewCmdUHostReboot() *cobra.Command { Use: "restart", Short: "Restart uhost instance", Long: "Restart uhost instance", - Example: "ucloud uhost restart --resource-id uhost-xxx1,uhost-xxx2", + Example: "ucloud uhost restart --uhost-id uhost-xxx1,uhost-xxx2", Run: func(cmd *cobra.Command, args []string) { for _, id := range *uhostIDs { id = base.PickResourceID(id) @@ -492,35 +1047,33 @@ func NewCmdUHostReboot() *cobra.Command { if err != nil { base.HandleError(err) } else { - text := fmt.Sprintf("UHost:[%v] is restarting", resp.UhostId) + text := fmt.Sprintf("uhost[%v] is restarting", resp.UHostId) if *async { - base.Cxt.Println(text) + fmt.Fprintln(out, text) } else { - done := pollUhost(resp.UhostId, *req.ProjectId, *req.Region, *req.Zone, []string{status.HOST_RUNNING, status.HOST_FAIL}) - ux.DotSpinner.Start(text) - <-done - ux.DotSpinner.Stop() + poller := base.NewPoller(describeUHostByID, out) + poller.Poll(resp.UHostId, *req.ProjectId, *req.Region, *req.Zone, text, []string{status.HOST_RUNNING, status.HOST_FAIL}) } } } }, } cmd.Flags().SortFlags = false - uhostIDs = cmd.Flags().StringSlice("resource-id", nil, "Required. ResourceIDs(UHostIds) of the uhost instance") - req.ProjectId = cmd.Flags().String("project-id", base.ConfigInstance.ProjectID, "Optional. Assign project-id") - req.Region = cmd.Flags().String("region", base.ConfigInstance.Region, "Optional. Assign region") + uhostIDs = cmd.Flags().StringSlice("uhost-id", nil, "Required. ResourceIDs(UHostIds) of the uhost instance") + req.ProjectId = cmd.Flags().String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + req.Region = cmd.Flags().String("region", base.ConfigIns.Region, "Optional. Assign region") req.Zone = cmd.Flags().String("zone", "", "Optional. Assign availability zone") req.DiskPassword = cmd.Flags().String("disk-password", "", "Optional. Encrypted disk password") async = cmd.Flags().Bool("async", false, "Optional. Do not wait for the long-running operation to finish.") - cmd.Flags().SetFlagValuesFunc("resource-id", func() []string { + cmd.Flags().SetFlagValuesFunc("uhost-id", func() []string { return getUhostList([]string{status.HOST_FAIL, status.HOST_RUNNING, status.HOST_STOPPED}, *req.ProjectId, *req.Region, *req.Zone) }) - cmd.MarkFlagRequired("resource-id") + cmd.MarkFlagRequired("uhost-id") return cmd } -//NewCmdUHostPoweroff ucloud uhost poweroff -func NewCmdUHostPoweroff() *cobra.Command { +// NewCmdUHostPoweroff ucloud uhost poweroff +func NewCmdUHostPoweroff(out io.Writer) *cobra.Command { var yes *bool var uhostIDs *[]string req := base.BizClient.NewPoweroffUHostInstanceRequest() @@ -528,7 +1081,7 @@ func NewCmdUHostPoweroff() *cobra.Command { Use: "poweroff", Short: "Analog power off Uhost instnace", Long: "Analog power off Uhost instnace", - Example: "ucloud uhost poweroff --resource-id uhost-xxx1,uhost-xxx2", + Example: "ucloud uhost poweroff --uhost-id uhost-xxx1,uhost-xxx2", Run: func(cmd *cobra.Command, args []string) { if !*yes { confirmText := "Danger, it may affect data integrity. Are you sure you want to poweroff this uhost?" @@ -537,7 +1090,7 @@ func NewCmdUHostPoweroff() *cobra.Command { } sure, err := ux.Prompt(confirmText) if err != nil { - base.Cxt.Println(err) + fmt.Fprintln(out, err) return } if !sure { @@ -551,31 +1104,75 @@ func NewCmdUHostPoweroff() *cobra.Command { if err != nil { base.HandleError(err) } else { - base.Cxt.Printf("UHost:[%v] is power off\n", resp.UhostId) + fmt.Fprintf(out, "uhost[%v] is power off\n", resp.UHostId) } } }, } cmd.Flags().SortFlags = false - uhostIDs = cmd.Flags().StringSlice("resource-id", nil, "ResourceIDs(UHostIds) of the uhost instance") - req.ProjectId = cmd.Flags().String("project-id", base.ConfigInstance.ProjectID, "Assign project-id") - req.Region = cmd.Flags().String("region", base.ConfigInstance.Region, "Assign region") + uhostIDs = cmd.Flags().StringSlice("uhost-id", nil, "ResourceIDs(UHostIds) of the uhost instance") + req.ProjectId = cmd.Flags().String("project-id", base.ConfigIns.ProjectID, "Assign project-id") + req.Region = cmd.Flags().String("region", base.ConfigIns.Region, "Assign region") req.Zone = cmd.Flags().String("zone", "", "Assign availability zone") yes = cmd.Flags().BoolP("yes", "y", false, "Optional. Do not prompt for confirmation.") - cmd.MarkFlagRequired("resource-id") + + cmd.Flags().SetFlagValuesFunc("uhost-id", func() []string { + return getUhostList([]string{status.HOST_FAIL, status.HOST_RUNNING, status.HOST_STOPPED}, *req.ProjectId, *req.Region, *req.Zone) + }) + cmd.MarkFlagRequired("uhost-id") + return cmd } -//NewCmdUHostResize ucloud uhost resize -func NewCmdUHostResize() *cobra.Command { - var yes *bool - var uhostIDs *[]string - req := base.BizClient.NewResizeUHostInstanceRequest() - cmd := &cobra.Command{ - Use: "resize", +func resizeAttachedDisk(out io.Writer, req *uhost.ResizeAttachedDiskRequest, host *uhost.UHostInstanceSet, yes, async bool, promptText string) error { + req.UHostId = &host.UHostId + if host.State == status.HOST_RUNNING { + err := tryStopUhost(req, host.UHostId, promptText, yes, async, out) + if err != nil { + return fmt.Errorf("try to stop uhost error :%w", err) + } + } + req.DryRun = sdk.Bool(false) + _, err := base.BizClient.ResizeAttachedDisk(req) + if err != nil { + return err + } + text := fmt.Sprintf("uhost [%s] disk [%s] resize", host.UHostId, *req.DiskId) + if async { + fmt.Fprintln(out, text) + } else { + poller := base.NewPoller(describeUHostByID, out) + poller.Poll(host.UHostId, *req.ProjectId, *req.Region, *req.Zone, text, []string{status.HOST_RUNNING, status.HOST_STOPPED, status.HOST_FAIL}) + } + return nil +} + +func tryStopUhost(req *uhost.ResizeAttachedDiskRequest, uhostID, promptText string, yes, async bool, out io.Writer) error { + req.DryRun = sdk.Bool(true) + resp, err := base.BizClient.ResizeAttachedDisk(req) + if err != nil { + return err + } + if resp.NeedRestart { + stopReq := base.BizClient.NewStopUHostInstanceRequest() + stopReq.UHostId = &uhostID + promptStopUhostIns(stopReq, yes, async, promptText, out) + } + return nil +} + +// NewCmdUHostResize ucloud uhost resize +func NewCmdUHostResize(out io.Writer) *cobra.Command { + var yes, async *bool + var bootDiskSize, dataDiskSize int + var dataDiskID string + var uhostIDs *[]string + req := base.BizClient.NewResizeUHostInstanceRequest() + cmd := &cobra.Command{ + Use: "resize", Short: "Resize uhost instance,such as cpu core count, memory size and disk size", Long: "Resize uhost instance,such as cpu core count, memory size and disk size", - Example: "ucloud uhost resize --resource-id uhost-xxx1,uhost-xxx2 --cpu 4 --memory-gb 8", + Example: "ucloud uhost resize --uhost-id uhost-xxx1,uhost-xxx2 --cpu 4 --memory-gb 8", Run: func(cmd *cobra.Command, args []string) { if *req.CPU == 0 { req.CPU = nil @@ -585,12 +1182,6 @@ func NewCmdUHostResize() *cobra.Command { } else { *req.Memory *= 1024 } - if *req.DiskSpace == 0 { - req.DiskSpace = nil - } - if *req.BootDiskSpace == 0 { - req.BootDiskSpace = nil - } for _, id := range *uhostIDs { id = base.PickResourceID(id) req.UHostId = &id @@ -600,58 +1191,115 @@ func NewCmdUHostResize() *cobra.Command { return } inst := host.(*uhost.UHostInstanceSet) - if inst.State == "Running" { - if !*yes { - confirmText := "Resize uhost must be after stop it. Do you want to stop this uhost?" - if len(*uhostIDs) > 1 { - confirmText = "Resize uhost must be after stop it. Do you want to stop those uhosts?" + stopReq := base.BizClient.NewStopUHostInstanceRequest() + stopReq.ProjectId = req.ProjectId + stopReq.Region = req.Region + stopReq.Zone = req.Zone + stopReq.UHostId = &id + confirmText := "Resize uhost must be done after the uhost is stopped. Do you want to stop this uhost?" + if req.CPU != nil || req.Memory != nil || *req.NetCapValue != 0 { + if inst.State == status.HOST_RUNNING { + ret := promptStopUhostIns(stopReq, *yes, *async, confirmText, out) + if ret { + inst.State = status.HOST_STOPPED } - agreeClose, err := ux.Prompt(confirmText) - if err != nil { - base.Cxt.Println(err) - return + } + resp, err := base.BizClient.ResizeUHostInstance(req) + if err != nil { + base.HandleError(err) + } else { + text := fmt.Sprintf("uhost [%v] cpu, memory resize", resp.UHostId) + if *async { + fmt.Fprintln(out, text) + } else { + poller := base.NewPoller(describeUHostByID, out) + poller.Poll(resp.UHostId, *req.ProjectId, *req.Region, *req.Zone, text, []string{status.HOST_RUNNING, status.HOST_STOPPED, status.HOST_FAIL}) + } + } + } + + if dataDiskSize != 0 || bootDiskSize != 0 { + _req := base.BizClient.NewResizeAttachedDiskRequest() + var bootDisk uhost.UHostDiskSet + var dataDisks = map[string]uhost.UHostDiskSet{} + for _, disk := range inst.DiskSet { + if disk.IsBoot == "True" { + bootDisk = disk + } else if disk.IsBoot == "False" { + dataDisks[disk.DiskId] = disk } - if !agreeClose { + } + if bootDiskSize != 0 { + if bootDiskSize <= bootDisk.Size { + base.LogError(fmt.Sprintf("Error, disk does not support shrinkage. current system-disk-size %dg", bootDisk.Size)) continue + } else { + _req.DiskSpace = &bootDiskSize + _req.DiskId = &bootDisk.DiskId + } + err := resizeAttachedDisk(out, _req, inst, *yes, *async, confirmText) + if err != nil { + base.HandleError(err) } } - _req := base.BizClient.NewStopUHostInstanceRequest() - _req.ProjectId = req.ProjectId - _req.Region = req.Region - _req.Zone = req.Zone - _req.UHostId = &id - stopUhostIns(_req, false) - } - resp, err := base.BizClient.ResizeUHostInstance(req) - if err != nil { - base.HandleError(err) - } else { - base.Cxt.Printf("UHost:[%v] resized\n", resp.UhostId) + if dataDiskSize != 0 { + var dataDisk uhost.UHostDiskSet + if len(dataDisks) > 1 { + if dataDiskID == "" { + base.LogError(fmt.Sprintf("Error, the uhost %s have %d data disks. data-disk-id should be assigned", id, len(dataDisks))) + continue + } + var ok bool + dataDisk, ok = dataDisks[dataDiskID] + if !ok { + base.LogError(fmt.Sprintf("Error, the disk %s does not exist", dataDiskID)) + continue + } + } else if len(dataDisks) == 1 { + for _, disk := range dataDisks { + dataDisk = disk + } + } else if len(dataDisks) == 0 { + base.LogError(fmt.Sprintf("Error, the uhost %s have no data disk. data-disk-id should be assigned", id)) + continue + } + if dataDiskSize <= dataDisk.Size { + base.LogError(fmt.Sprintf("Error, disk does not support shrinkage. current data-disk-size %dg", dataDisk.Size)) + continue + } + _req.DiskSpace = &dataDiskSize + _req.DiskId = &dataDisk.DiskId + err := resizeAttachedDisk(out, _req, inst, *yes, *async, confirmText) + if err != nil { + base.HandleError(err) + } + } } } }, } - cmd.Flags().SortFlags = false - uhostIDs = cmd.Flags().StringSlice("resource-id", nil, "Required. ResourceIDs(or UhostIDs) of the uhost instances") - req.ProjectId = cmd.Flags().String("project-id", base.ConfigInstance.ProjectID, "Optional. Assign project-id") - req.Region = cmd.Flags().String("region", base.ConfigInstance.Region, "Optional. Assign region") - req.Zone = cmd.Flags().String("zone", "", "Optional. Assign availability zone") + flags := cmd.Flags() + flags.SortFlags = false + uhostIDs = cmd.Flags().StringSlice("uhost-id", nil, "Required. ResourceIDs(or UhostIDs) of the uhost instances") + bindProjectID(req, flags) + bindRegion(req, flags) + bindZone(req, flags) req.CPU = cmd.Flags().Int("cpu", 0, "Optional. The number of virtual CPU cores. Series1 {1, 2, 4, 8, 12, 16, 24, 32}. Series2 {1,2,4,8,16}") req.Memory = cmd.Flags().Int("memory-gb", 0, "Optional. memory size. Unit: GB. Range: [1, 128], multiple of 2") - req.DiskSpace = cmd.Flags().Int("data-disk-size-gb", 0, "Optional. Data disk size,unit GB. Range[10,1000], SSD disk range[100,500]. Step 10") - req.BootDiskSpace = cmd.Flags().Int("system-disk-size-gb", 0, "Optional. System disk size, unit GB. Range[20,100]. Step 10. System disk does not support shrinkage") + cmd.Flags().IntVar(&bootDiskSize, "system-disk-size-gb", 0, "Optional. System disk size, unit GB. Range[20,100]. Step 10. System disk does not support shrinkage") + cmd.Flags().IntVar(&dataDiskSize, "data-disk-size-gb", 0, "Optional. Data disk size,unit GB. Step 10. disk does not support shrinkage") + cmd.Flags().StringVar(&dataDiskID, "data-disk-id", "", "Optional. If the uhost specified has two or more data disks, this parameter should be assigned") req.NetCapValue = cmd.Flags().Int("net-cap", 0, "Optional. NIC scale. 1,upgrade; 2,downgrade; 0,unchanged") yes = cmd.Flags().BoolP("yes", "y", false, "Optional. Do not prompt for confirmation.") - cmd.Flags().SetFlagValuesFunc("resource-id", func() []string { + async = cmd.Flags().BoolP("async", "a", false, "Optional. Do not wait for the long-running operation to finish.") + cmd.Flags().SetFlagValuesFunc("uhost-id", func() []string { return getUhostList([]string{status.HOST_RUNNING, status.HOST_STOPPED, status.HOST_FAIL}, *req.ProjectId, *req.Region, *req.Zone) }) - cmd.MarkFlagRequired("resource-id") + cmd.MarkFlagRequired("uhost-id") return cmd } -var pollUhost = base.Poll(describeUHostByID) - func describeUHostByID(uhostID, projectID, region, zone string) (interface{}, error) { req := base.BizClient.NewDescribeUHostInstanceRequest() req.UHostIds = []string{uhostID} @@ -659,6 +1307,24 @@ func describeUHostByID(uhostID, projectID, region, zone string) (interface{}, er req.Region = ®ion req.Zone = &zone + resp, err := base.BizClient.DescribeUHostInstance(req) + if err != nil { + return nil, err + } + if len(resp.UHostSet) < 1 { + return nil, fmt.Errorf("uhost [%s] does not exist", uhostID) + } + + return &resp.UHostSet[0], nil +} + +func sdescribeUHostByID(uhostID string, commonBase *request.CommonBase) (interface{}, error) { + req := base.BizClient.NewDescribeUHostInstanceRequest() + if commonBase != nil { + req.CommonBase = *commonBase + } + req.UHostIds = []string{uhostID} + resp, err := base.BizClient.DescribeUHostInstance(req) if err != nil { return nil, err @@ -683,11 +1349,569 @@ func getUhostList(states []string, project, region, zone string) []string { } list := []string{} for _, host := range resp.UHostSet { - for _, s := range states { - if host.State == s { - list = append(list, host.UHostId+"/"+strings.Replace(host.Name, " ", "-", -1)) + if states != nil { + for _, s := range states { + if host.State == s { + list = append(list, host.UHostId+"/"+strings.Replace(host.Name, " ", "-", -1)) + } + } + } else { + list = append(list, host.UHostId+"/"+strings.Replace(host.Name, " ", "-", -1)) + } + } + return list +} + +// NewCmdUHostClone ucloud uhost clone +func NewCmdUHostClone(out io.Writer) *cobra.Command { + var uhostID *string + var async *bool + + var password string + var keyPairId string + + req := base.BizClient.NewCreateUHostInstanceRequest() + cmd := &cobra.Command{ + Use: "clone", + Short: "Create an uhost with the same configuration as another uhost, excluding bound eip and udisk", + Long: "Create an uhost with the same configuration as another uhost, excluding bound eip and udisk", + Run: func(com *cobra.Command, args []string) { + if len(password) > 0 { + req.LoginMode = sdk.String("Password") + req.KeyPairId = nil + req.Password = sdk.String(password) + } else if len(keyPairId) > 0 { + req.LoginMode = sdk.String("KeyPair") + req.KeyPairId = sdk.String(keyPairId) + req.Password = nil + } else { + base.Cxt.PrintErr(errors.New("password or key-pair-id is required")) + return + } + *uhostID = base.PickResourceID(*uhostID) + queryReq := base.BizClient.NewDescribeUHostInstanceRequest() + queryReq.ProjectId = req.ProjectId + queryReq.Region = req.Region + queryReq.Zone = req.Zone + queryReq.UHostIds = []string{*uhostID} + queryResp, err := base.BizClient.DescribeUHostInstance(queryReq) + if err != nil { + base.HandleError(err) + return + } + if len(queryResp.UHostSet) < 1 { + base.Cxt.PrintErr(fmt.Errorf("uhost[%s] not exist", *uhostID)) + return + } + if queryResp.UHostSet[0].SecGroupInstance == true { + base.Cxt.PrintErr(fmt.Errorf("uhost[%s] is in security groups, it is not allowed to clone", *uhostID)) + return + } + queryFirewallReq := base.BizClient.NewDescribeFirewallRequest() + queryFirewallReq.ProjectId = req.ProjectId + queryFirewallReq.Region = req.Region + queryFirewallReq.ResourceId = uhostID + queryFirewallReq.ResourceType = sdk.String("uhost") + + firewallResp, err := base.BizClient.DescribeFirewall(queryFirewallReq) + if err != nil { + base.HandleError(err) + return + } + + if len(firewallResp.DataSet) == 1 { + req.SecurityGroupId = &firewallResp.DataSet[0].FWId + } + + uhostIns := queryResp.UHostSet[0] + + req.ImageId = &uhostIns.BasicImageId + req.CPU = &uhostIns.CPU + req.Memory = &uhostIns.Memory + for _, ip := range uhostIns.IPSet { + if ip.Type == "Private" { + req.VPCId = &ip.VPCId + req.SubnetId = &ip.SubnetId + } + } + req.ChargeType = &uhostIns.ChargeType + req.UHostType = &uhostIns.UHostType + req.NetCapability = &uhostIns.NetCapability + + for _, disk := range uhostIns.DiskSet { + item := uhost.UHostDisk{ + Size: sdk.Int(disk.Size), + Type: sdk.String(disk.DiskType), + IsBoot: sdk.String(disk.IsBoot), + } + if disk.BackupType != "" { + item.BackupType = sdk.String(disk.BackupType) + } + req.Disks = append(req.Disks, item) + } + req.Tag = &uhostIns.Tag + resp, err := base.BizClient.CreateUHostInstance(req) + if err != nil { + base.HandleError(err) + return + } + if len(resp.UHostIds) == 1 { + text := fmt.Sprintf("cloned uhost:[%s] is initializing", resp.UHostIds[0]) + if *async { + fmt.Fprintln(out, text) + } else { + poller := base.NewPoller(describeUHostByID, out) + poller.Poll(resp.UHostIds[0], *req.ProjectId, *req.Region, *req.Zone, text, []string{status.HOST_RUNNING, status.HOST_FAIL}) + } + } else { + base.HandleError(fmt.Errorf("expect uhost count 1, accept %d", len(resp.UHostIds))) + return + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + uhostID = flags.String("uhost-id", "", "Required. Resource ID of the uhost to clone from") + flags.StringVar(&password, "password", "", "Optional. Password of the uhost user(root/ubuntu)") + flags.StringVar(&keyPairId, "key-pair-id", "", "Optional. Resource ID of ssh key pair. See 'ucloud api --Action DescribeUHostKeyPairs' Where both password and key-pair-id are set, the key-pair-id is ignored") + + req.Name = flags.String("name", "", "Optional. Name of the uhost to clone") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Assign region") + req.Zone = flags.String("zone", base.ConfigIns.Zone, "Optional. Assign availability zone") + async = flags.Bool("async", false, "Optional. Do not wait for the long-running operation to finish.") + flags.SetFlagValuesFunc("uhost-id", func() []string { + return getUhostList([]string{status.HOST_RUNNING, status.HOST_STOPPED}, *req.ProjectId, *req.Region, *req.Zone) + }) + cmd.MarkFlagRequired("uhost-id") + return cmd +} + +// NewCmdUhostCreateImage ucloud uhost create-image +func NewCmdUhostCreateImage(out io.Writer) *cobra.Command { + var async *bool + req := base.BizClient.NewCreateCustomImageRequest() + cmd := &cobra.Command{ + Use: "create-image", + Short: "Create image from an uhost instance", + Long: "Create image from an uhost instance", + Run: func(cmd *cobra.Command, args []string) { + req.UHostId = sdk.String(base.PickResourceID(*req.UHostId)) + resp, err := base.BizClient.CreateCustomImage(req) + if err != nil { + base.HandleError(err) + return + } + text := fmt.Sprintf("iamge[%s] is making", resp.ImageId) + if *async { + fmt.Fprintln(out, text) + } else { + poller := base.NewPoller(describeImageByID, out) + poller.Poll(resp.ImageId, *req.ProjectId, *req.Region, *req.Zone, text, []string{status.IMAGE_AVAILABLE, status.IMAGE_UNAVAILABLE}) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + req.UHostId = flags.String("uhost-id", "", "Resource ID of uhost to create image from") + req.ImageName = flags.String("image-name", "", "Required. Name of the image to create") + req.ImageDescription = flags.String("image-desc", "", "Optional. Description of the image to create") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Assign region") + req.Zone = flags.String("zone", base.ConfigIns.Zone, "Optional. Assign availability zone") + async = flags.BoolP("async", "a", false, "Optional. Do not wait for the long-running operation to finish.") + + flags.SetFlagValuesFunc("uhost-id", func() []string { + return getUhostList([]string{status.HOST_RUNNING, status.HOST_STOPPED}, *req.ProjectId, *req.Region, *req.Zone) + }) + + cmd.MarkFlagRequired("uhost-id") + cmd.MarkFlagRequired("image-name") + return cmd +} + +// NewCmdUhostResetPassword ucloud uhost reset-password +func NewCmdUhostResetPassword(out io.Writer) *cobra.Command { + var yes *bool + var uhostIDs *[]string + req := base.BizClient.NewResetUHostInstancePasswordRequest() + cmd := &cobra.Command{ + Use: "reset-password", + Short: "Reset the administrator password for the UHost instances.", + Long: "Reset the administrator password for the UHost instances.", + Run: func(cmd *cobra.Command, args []string) { + for _, id := range *uhostIDs { + id = base.PickResourceID(id) + req.UHostId = &id + err := checkAndCloseUhost(*yes, false, id, *req.ProjectId, *req.Region, *req.Zone, out) + if err != nil { + base.Cxt.Println(err) + continue + } + host, err := describeUHostByID(id, *req.ProjectId, *req.Region, *req.Zone) + inst, ok := host.(*uhost.UHostInstanceSet) + if !ok { + return + } + if inst.BootDiskState == "Initializing" { + fmt.Fprintf(out, "uhost[%s] boot disk in initializing, wait 10 minutes\n", id) + return + } + resp, err := base.BizClient.ResetUHostInstancePassword(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintf(out, "uhost[%s] reset password\n", resp.UHostId) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + uhostIDs = flags.StringSlice("uhost-id", nil, "Required. Resource IDs of the uhosts to reset the administrator's password") + req.Password = flags.String("password", "", "Required. New Password") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Assign region") + req.Zone = flags.String("zone", base.ConfigIns.Zone, "Optional. Assign availability zone") + yes = cmd.Flags().BoolP("yes", "y", false, "Optional. Do not prompt for confirmation.") + flags.SetFlagValuesFunc("uhost-id", func() []string { + return getUhostList([]string{status.HOST_RUNNING, status.HOST_STOPPED}, *req.ProjectId, *req.Region, *req.Zone) + }) + cmd.MarkFlagRequired("uhost-id") + cmd.MarkFlagRequired("password") + return cmd +} + +func checkAndCloseUhost(yes, async bool, uhostID, project, region, zone string, out io.Writer) error { + host, err := describeUHostByID(uhostID, project, region, zone) + if err != nil { + return err + } + inst, ok := host.(*uhost.UHostInstanceSet) + if ok { + if inst.State == "Running" { + if !yes { + confirmText := fmt.Sprintf("uhost[%s] will be stopped, can we do this?", uhostID) + agreeClose, err := ux.Prompt(confirmText) + if err != nil { + return err + } + if !agreeClose { + return fmt.Errorf("skip, you do not agree to stop uhost") + } } + _req := base.BizClient.NewStopUHostInstanceRequest() + _req.ProjectId = &project + _req.Region = ®ion + _req.Zone = &zone + _req.UHostId = &uhostID + stopUhostIns(_req, async, out) } + } else { + return fmt.Errorf("Something wrong, uhost[%s] may not exist", uhostID) + } + return nil +} + +// NewCmdUhostReinstallOS ucloud uhost reinstall-os +func NewCmdUhostReinstallOS(out io.Writer) *cobra.Command { + var isReserveDataDisk, yes, async *bool + var password, keyPairId string + req := base.BizClient.NewReinstallUHostInstanceRequest() + cmd := &cobra.Command{ + Use: "reinstall-os", + Short: "Reinstall the operating system of the UHost instance", + Long: "Reinstall the operating system of the UHost instance. we will detach all udisk disks if the uhost attached some, and then stop the uhost if it's running", + Run: func(cmd *cobra.Command, args []string) { + if *isReserveDataDisk { + req.ReserveDisk = sdk.String("Yes") + } else { + req.ReserveDisk = sdk.String("No") + } + req.UHostId = sdk.String(base.PickResourceID(*req.UHostId)) + if len(password) > 0 { + req.LoginMode = sdk.String("Password") + req.KeyPairId = nil + req.Password = sdk.String(password) + } else if len(keyPairId) > 0 { + req.LoginMode = sdk.String("KeyPair") + req.KeyPairId = sdk.String(keyPairId) + req.Password = nil + } else { + base.Cxt.PrintErr(fmt.Errorf("password or key-pair-id is required")) + return + } + + any, err := describeUHostByID(*req.UHostId, *req.ProjectId, *req.Region, *req.Zone) + if err != nil { + base.Cxt.Println(err) + return + } + uhostIns, ok := any.(*uhost.UHostInstanceSet) + if ok { + for _, disk := range uhostIns.DiskSet { + if disk.Type == "Udisk" { + sure := false + if !*yes { + text := fmt.Sprintf("udisk[%s/%s] will be detached, can we do this?", disk.DiskId, disk.Name) + sure, err = ux.Prompt(text) + if err != nil { + base.Cxt.PrintErr(err) + return + } + if !sure { + base.Cxt.Printf("you don't agree to detach udisk\n") + return + } + } + if *yes || sure { + err := detachUdisk(false, disk.DiskId, out) + if err != nil { + base.Cxt.Println(err) + return + } + } + } + } + } else { + base.Cxt.Printf("Something wrong, uhost[%s] may not exist\n", *req.UHostId) + return + } + + err = checkAndCloseUhost(*yes, *async, *req.UHostId, *req.ProjectId, *req.Region, *req.Zone, out) + if err != nil { + base.Cxt.Println(err) + return + } + resp, err := base.BizClient.ReinstallUHostInstance(req) + if err != nil { + base.Cxt.Println(err) + return + } + text := fmt.Sprintf("uhost[%s] is reinstalling OS", *req.UHostId) + if *async { + fmt.Fprintln(out, text) + } else { + poller := base.NewPoller(describeUHostByID, out) + poller.Poll(resp.UHostId, *req.ProjectId, *req.Region, *req.Zone, text, []string{status.HOST_RUNNING, status.HOST_FAIL}) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + req.UHostId = flags.String("uhost-id", "", "Required. Resource ID of the uhost to reinstall operating system") + flags.StringVar(&password, "password", "", "Optional. Password of the uhost user(root/ubuntu)") + flags.StringVar(&keyPairId, "key-pair-id", "", "Optional. Resource ID of ssh key pair. See 'ucloud api --Action DescribeUHostKeyPairs' Where both password and key-pair-id are set, the key-pair-id is ignored") + req.ImageId = flags.String("image-id", "", "Optional. Resource ID the image to install. See 'ucloud image list'. Default is original image of the uhost") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Assign region") + req.Zone = flags.String("zone", base.ConfigIns.Zone, "Optional. Assign availability zone") + isReserveDataDisk = flags.Bool("keep-data-disk", false, "Keep data disk or not. If you keep data disk, you can't change OS type(Linux->Window,e.g.)") + yes = cmd.Flags().BoolP("yes", "y", false, "Optional. Do not prompt for confirmation.") + async = flags.BoolP("async", "a", false, "Optional. Do not wait for the long-running operation to finish.") + flags.SetFlagValuesFunc("uhost-id", func() []string { + return getUhostList([]string{status.HOST_RUNNING, status.HOST_STOPPED}, *req.ProjectId, *req.Region, *req.Zone) + }) + cmd.MarkFlagRequired("uhost-id") + return cmd +} + +// NewCmdUhostLeaveIsolationGroup ucloud uhost leave-isolation-group +func NewCmdUhostLeaveIsolationGroup(out io.Writer) *cobra.Command { + var uhostIds []string + req := base.BizClient.NewLeaveIsolationGroupRequest() + cmd := &cobra.Command{ + Use: "leave-isolation-group", + Short: "Detach uhost from its isolation group", + Run: func(c *cobra.Command, args []string) { + for _, idname := range uhostIds { + id := base.PickResourceID(idname) + any, err := describeUHostByID(id, *req.ProjectId, *req.Region, *req.Zone) + if err != nil { + base.LogError(fmt.Sprintf("fetch uhost %s failed: %v", idname, err)) + continue + } + ins, ok := any.(*uhost.UHostInstanceSet) + if !ok { + base.LogError(fmt.Sprintf("uhost %s may not exist", idname)) + continue + } + if ins.IsolationGroup == "" { + base.LogPrint(fmt.Sprintf("uhost %s doesn't attached any isolation group", idname)) + continue + } + req.GroupId = sdk.String(ins.IsolationGroup) + req.UHostId = &id + _, err = base.BizClient.LeaveIsolationGroup(req) + if err != nil { + base.HandleError(err) + continue + } + base.LogPrint(fmt.Sprintf("uhost %s detached from isolation group %s", idname, ins.IsolationGroup)) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + flags.StringSliceVar(&uhostIds, "uhost-id", nil, "Required. Resource ID of uhosts to be detech from its isolation group") + bindRegion(req, flags) + bindProjectID(req, flags) + bindZone(req, flags) + cmd.MarkFlagRequired("uhost-id") + flags.SetFlagValuesFunc("uhost-id", func() []string { + return getUhostList(nil, *req.ProjectId, *req.Region, *req.Zone) + }) + return cmd +} + +// NewCmdIsolation ucloud uhost isolation-gorup +func NewCmdIsolation(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "isolation-group", + Short: "List and manipulate isolation group of uhost", + Long: "List and manipulate isolation group of uhost", + } + cmd.AddCommand(NewCmdIsolationList(out)) + cmd.AddCommand(NewCmdIsolationCreate(out)) + cmd.AddCommand(NewCmdIsolationDelete(out)) + return cmd +} + +// NewCmdIsolationCreate ucloud uhost isolation-group create +func NewCmdIsolationCreate(out io.Writer) *cobra.Command { + req := base.BizClient.NewCreateIsolationGroupRequest() + cmd := &cobra.Command{ + Use: "create", + Short: "Create isolation group instance", + Long: "Create isolation group instance", + Run: func(c *cobra.Command, args []string) { + re := regexp.MustCompile(cli.REGEXP_NAME) + if !re.Match([]byte(*req.GroupName)) { + base.LogError(fmt.Sprintf("group-name %s is invalid! Length 1~63, only English,Chinese,number and '-_.' are allowed", *req.GroupName)) + return + } + resp, err := base.BizClient.CreateIsolationGroup(req) + if err != nil { + base.HandleError(err) + return + } + base.LogPrint(fmt.Sprintf("isolation group %s created", resp.GroupId)) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + req.GroupName = flags.String("group-name", "", "Required. Name of isolation group. Length 1~63, only English,Chinese,number and '-_.' are allowed") + bindRegion(req, flags) + bindProjectID(req, flags) + req.Remark = flags.String("remark", "", "Optional. Remark ok isolation group") + + cmd.MarkFlagRequired("group-name") + return cmd +} + +// NewCmdIsolationDelete ucloud uhost +func NewCmdIsolationDelete(out io.Writer) *cobra.Command { + var ids []string + req := base.BizClient.NewDeleteIsolationGroupRequest() + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete isolation group instances", + Run: func(c *cobra.Command, args []string) { + for _, idname := range ids { + id := base.PickResourceID(idname) + req.GroupId = &id + _, err := base.BizClient.DeleteIsolationGroup(req) + if err != nil { + base.HandleError(err) + continue + } + base.LogPrint(fmt.Sprintf("isolation group %s deleted", idname)) + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + flags.StringSliceVar(&ids, "group-id", nil, "Required. Resource ID of isolation groups to be deleted") + bindRegion(req, flags) + bindProjectID(req, flags) + + cmd.MarkFlagRequired("group-id") + flags.SetFlagValuesFunc("group-id", func() []string { + return getIsolationGroupList(*req.ProjectId, *req.Region) + }) + + return cmd +} + +type isolationGroupRow struct { + ResourceID string + Name string + Remark string + UHostCount string +} + +// NewCmdIsolationList ucloud uhost isolation-group list +func NewCmdIsolationList(out io.Writer) *cobra.Command { + req := base.BizClient.NewDescribeIsolationGroupRequest() + cmd := &cobra.Command{ + Use: "list", + Short: "List isolation group of uhost", + Run: func(c *cobra.Command, args []string) { + resp, err := base.BizClient.DescribeIsolationGroup(req) + if err != nil { + base.HandleError(err) + return + } + var list []isolationGroupRow + for _, group := range resp.IsolationGroupSet { + row := isolationGroupRow{ + ResourceID: group.GroupId, + Name: group.GroupName, + Remark: group.Remark, + } + var zones []string + for _, item := range group.SpreadInfoSet { + zones = append(zones, fmt.Sprintf("%s:%d", item.Zone, item.UHostCount)) + } + row.UHostCount = strings.Join(zones, " ") + list = append(list, row) + } + base.PrintList(list, out) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + req.GroupId = flags.String("group-id", "", "Optional. Resource ID of isolation group to describe") + bindRegion(req, flags) + bindProjectID(req, flags) + bindLimit(req, flags) + bindOffset(req, flags) + + flags.SetFlagValuesFunc("group-id", func() []string { + return getIsolationGroupList(*req.ProjectId, *req.Region) + }) + + return cmd +} + +func getIsolationGroupList(project, region string) []string { + req := base.BizClient.NewDescribeIsolationGroupRequest() + req.ProjectId = sdk.String(project) + req.Region = sdk.String(region) + req.Limit = sdk.Int(50) + resp, err := base.BizClient.DescribeIsolationGroup(req) + if err != nil { + fmt.Println(err) + return nil + } + list := []string{} + for _, group := range resp.IsolationGroupSet { + list = append(list, group.GroupId+"/"+strings.Replace(group.GroupName, " ", "-", -1)) } return list } diff --git a/cmd/uhost_test.go b/cmd/uhost_test.go index 1c930a9daa..ef1ff13891 100644 --- a/cmd/uhost_test.go +++ b/cmd/uhost_test.go @@ -1,7 +1,16 @@ package cmd import ( + "bytes" + "encoding/json" + "fmt" + "regexp" + "strings" "testing" + "time" + + "github.com/ucloud/ucloud-cli/base" + "github.com/ucloud/ucloud-cli/ux" ) type listUhostTest struct { @@ -10,14 +19,215 @@ type listUhostTest struct { } func (test listUhostTest) run(t *testing.T) { - cmd := NewCmdUHostList() - cmd.SetArgs([]string{"--project-id", "org-4nfe1i"}) + buf := new(bytes.Buffer) + cmd := NewCmdUHostList(buf) if err := cmd.Execute(); err != nil { t.Fatalf("unexpected error executing command:%v", err) } } -func TestListUhost(t *testing.T) { - test := listUhostTest{} - test.run(t) +type listImageTest struct { + flags []string +} + +func (test *listImageTest) run(t *testing.T) string { + global.JSON = true + buf := new(bytes.Buffer) + cmd := NewCmdUImageList(buf) + cmd.Flags().Parse(test.flags) + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpected error executing command: %v, flags: %v", err, test.flags) + } + + var images []ImageRow + err := json.Unmarshal(buf.Bytes(), &images) + if err != nil { + t.Fatalf("unexpected error of fetching image list: %v", err) + } + if len(images) == 0 { + t.Fatalf("image list is empty") + } + // for _, image := range images { + // // image.ImageName + // } + return images[0].ImageID +} + +type createUHostTest struct { + flags []string + uhostIDs []string + expectedOutRegexp *regexp.Regexp +} + +func (test *createUHostTest) run(t *testing.T) { + cmd := NewCmdUHostCreate() + cmd.Flags().Parse(test.flags) + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpected error executing command: %v, flags: %v", err, test.flags) + } + lines := ux.Doc.Content() + content := strings.Join(lines, "\n") + list := test.expectedOutRegexp.FindStringSubmatch(content) + if list == nil { + t.Errorf("unexpect output:%s", content) + } else { + if len(list) == 2 { + test.uhostIDs = append(test.uhostIDs, list[1]) + } + } +} + +type deleteUHostTest struct { + flags []string + uhostIDs []string + expectedOutRegexp *regexp.Regexp +} + +func (test *deleteUHostTest) run(t *testing.T) { + cmd := NewCmdUHostDelete() + cmd.Flags().Parse(test.flags) + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpected error executing command: %v, flags: %v", err, test.flags) + } + lines := ux.Doc.Content() + content := strings.Join(lines, "\n") + list := test.expectedOutRegexp.FindStringSubmatch(content) + if list == nil { + t.Errorf("unexpect output:%s", content) + } +} + +type stopUHostTest struct { + flags []string + uhostIDs []string + expectedOutRegexp *regexp.Regexp +} + +func (test *stopUHostTest) run(t *testing.T) { + buf := new(bytes.Buffer) + cmd := NewCmdUHostStop(buf) + cmd.Flags().Parse(test.flags) + + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpected error executing command: %v, flags: %v", err, test.flags) + } + list := test.expectedOutRegexp.FindStringSubmatch(buf.String()) + if list == nil { + t.Errorf("unexpect output:%s", buf.String()) + } +} + +type startUHostTest struct { + flags []string + uhostIDs []string + expectedOutRegexp *regexp.Regexp +} + +func (test *startUHostTest) run(t *testing.T) { + buf := new(bytes.Buffer) + cmd := NewCmdUHostStart(buf) + cmd.Flags().Parse(test.flags) + + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpected error executing command: %v, flags: %v", err, test.flags) + } + list := test.expectedOutRegexp.FindStringSubmatch(buf.String()) + if list == nil { + t.Errorf("unexpect output:%s", buf.String()) + } +} + +type restartUHostTest struct { + flags []string + uhostIDs []string + expectedOutRegexp *regexp.Regexp +} + +func (test *restartUHostTest) run(t *testing.T) { + buf := new(bytes.Buffer) + cmd := NewCmdUHostReboot(buf) + cmd.Flags().Parse(test.flags) + + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpected error executing command: %v, flags: %v", err, test.flags) + } + list := test.expectedOutRegexp.FindStringSubmatch(buf.String()) + if list == nil { + t.Errorf("unexpect output:%s", buf.String()) + } +} + +type poweroffUHostTest struct { + flags []string + uhostIDs []string + expectedOutRegexp *regexp.Regexp +} + +func (test *poweroffUHostTest) run(t *testing.T) { + buf := new(bytes.Buffer) + cmd := NewCmdUHostPoweroff(buf) + cmd.Flags().Parse(test.flags) + + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpected error executing command: %v, flags: %v", err, test.flags) + } + list := test.expectedOutRegexp.FindStringSubmatch(buf.String()) + if list == nil { + t.Errorf("unexpect output:%s", buf.String()) + } +} + +func TestUhost(t *testing.T) { + base.InitConfig() + listImageT := listImageTest{ + flags: []string{"--json"}, + } + imageID := listImageT.run(t) + + createT := createUHostTest{expectedOutRegexp: regexp.MustCompile(`uhost\[([\w-]+)\] which attached a data disk and binded an eip is initializing\.\.\.done`), + flags: []string{ + "--cpu=1", + "--memory-gb=1", + "--image-id=" + imageID, + "--password=testlxj@123", + "--hot-plug=false", + "--create-eip-bandwidth-mb=10", + }, + } + createT.run(t) + + restartT := restartUHostTest{ + flags: []string{fmt.Sprintf("--uhost-id=%s", strings.Join(createT.uhostIDs, ","))}, + expectedOutRegexp: regexp.MustCompile(`uhost\[([\w-]+)\] is restarting\.\.\.done`), + } + restartT.run(t) + + poweroffT := poweroffUHostTest{ + flags: []string{"--yes", fmt.Sprintf("--uhost-id=%s", strings.Join(createT.uhostIDs, ","))}, + expectedOutRegexp: regexp.MustCompile(`uhost\[([\w-]+)\] is power off`), + } + poweroffT.run(t) + + time.Sleep(time.Second * 5) + startT := startUHostTest{ + flags: []string{fmt.Sprintf("--uhost-id=%s", strings.Join(createT.uhostIDs, ","))}, + expectedOutRegexp: regexp.MustCompile(`uhost\[([\w-]+)\] is starting\.\.\.done`), + } + startT.run(t) + + stopT := stopUHostTest{ + flags: []string{fmt.Sprintf("--uhost-id=%s", strings.Join(createT.uhostIDs, ","))}, + expectedOutRegexp: regexp.MustCompile(`uhost\[([\w-]+)\] is shutting down\.\.\.done`), + } + + stopT.run(t) + + deleteT := deleteUHostTest{ + uhostIDs: createT.uhostIDs, + expectedOutRegexp: regexp.MustCompile(`uhost\[([\w-]+)\] deleted`), + flags: []string{"--yes"}, + } + deleteT.flags = append(deleteT.flags, fmt.Sprintf("--uhost-id=%s", strings.Join(deleteT.uhostIDs, ","))) + deleteT.run(t) + } diff --git a/cmd/ulb.go b/cmd/ulb.go new file mode 100644 index 0000000000..168628326b --- /dev/null +++ b/cmd/ulb.go @@ -0,0 +1,1698 @@ +// Copyright © 2018 NAME HERE tony.li@ucloud.cn +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "io" + "io/ioutil" + "strings" + + "github.com/spf13/cobra" + + "github.com/ucloud/ucloud-sdk-go/services/ulb" + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + + "github.com/ucloud/ucloud-cli/base" + "github.com/ucloud/ucloud-cli/model/status" +) + +// NewCmdULB ucloud ulb +func NewCmdULB() *cobra.Command { + cmd := &cobra.Command{ + Use: "ulb", + Short: "List and manipulate ULB instances", + Long: "List and manipulate ULB instances", + } + out := base.Cxt.GetWriter() + + cmd.AddCommand(NewCmdULBList(out)) + cmd.AddCommand(NewCmdULBCreate(out)) + cmd.AddCommand(NewCmdULBUpdate(out)) + cmd.AddCommand(NewCmdULBDelete(out)) + cmd.AddCommand(NewCmdULBVserver()) + cmd.AddCommand(NewCmdULBSSL()) + + return cmd +} + +// ULBRow 表格行 +type ULBRow struct { + Name string + ResourceID string + Group string + Network string + VserverCount int + VPC string + CreationTime string +} + +// NewCmdULBList ucloud ulb list +func NewCmdULBList(out io.Writer) *cobra.Command { + req := base.BizClient.NewDescribeULBRequest() + cmd := &cobra.Command{ + Use: "list", + Short: "List ULB instances", + Long: "List ULB instances", + Run: func(c *cobra.Command, args []string) { + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + req.VPCId = sdk.String(base.PickResourceID(*req.VPCId)) + resp, err := base.BizClient.DescribeULB(req) + if err != nil { + base.HandleError(err) + return + } + list := []ULBRow{} + for _, ulb := range resp.DataSet { + row := ULBRow{} + row.ResourceID = ulb.ULBId + row.Name = ulb.Name + row.Group = ulb.BusinessId + row.VserverCount = len(ulb.VServerSet) + row.VPC = ulb.VPCId + row.CreationTime = base.FormatDate(ulb.CreateTime) + if ulb.ULBType == "OuterMode" { + ips := []string{} + for _, ip := range ulb.IPSet { + ips = append(ips, fmt.Sprintf("%s(%s)", ip.EIP, ip.EIPId)) + } + row.Network = strings.Join(ips, ",") + } else { + row.Network = ulb.PrivateIP + } + list = append(list, row) + } + + base.PrintList(list, out) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + bindRegion(req, flags) + bindProjectID(req, flags) + + req.ULBId = flags.String("ulb-id", "", "Optional. Resource ID of ULB instance to list") + req.VPCId = flags.String("vpc-id", "", "Optional. Resource ID of VPC which the ULB instances to list belong to") + req.SubnetId = flags.String("subnet-id", "", "Optional. Resource ID of subnet which the ULB instances to list belong to") + req.BusinessId = flags.String("group", "", "Optional. Business group of ULB instances to list") + req.Offset = flags.Int("offset", 0, "Optional. Offset") + req.Limit = flags.Int("limit", 50, "Optional. Limit") + + flags.SetFlagValuesFunc("vpc-id", func() []string { + return getAllVPCIdNames(*req.ProjectId, *req.Region) + }) + + return cmd +} + +// NewCmdULBCreate ucloud ulb create +func NewCmdULBCreate(out io.Writer) *cobra.Command { + var bindEipID *string + mode := "outer" + req := base.BizClient.NewCreateULBRequest() + eipReq := base.BizClient.NewAllocateEIPRequest() + cmd := &cobra.Command{ + Use: "create", + Short: "Create ULB instance", + Long: "Create ULB instance", + Run: func(c *cobra.Command, args []string) { + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + if mode == "outer" { + if *bindEipID == "" && *eipReq.Bandwidth == 0 { + fmt.Fprintln(out, "Outer mode ULB need a eip to bind, please assign eip by flag 'bind-eip' or create eip by 'create-eip-bandwidth-mb'") + return + } + if *eipReq.OperatorName == "" { + *eipReq.OperatorName = getEIPLine(*req.Region) + } + req.OuterMode = sdk.String("Yes") + } else if mode == "inner" { + req.InnerMode = sdk.String("Yes") + } else { + fmt.Fprintln(out, "Error, flag mode should be 'outer' or 'inner'") + return + } + req.VPCId = sdk.String(base.PickResourceID(*req.VPCId)) + req.SubnetId = sdk.String(base.PickResourceID(*req.SubnetId)) + resp, err := base.BizClient.CreateULB(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintf(out, "ulb[%s] created\n", resp.ULBId) + if mode == "inner" { + return + } + bindEipID = sdk.String(base.PickResourceID(*bindEipID)) + if *bindEipID != "" { + bindEIP(sdk.String(resp.ULBId), sdk.String("ulb"), bindEipID, req.ProjectId, req.Region) + return + } + if *eipReq.OperatorName != "" && *eipReq.Bandwidth != 0 { + eipReq.ChargeType = req.ChargeType + eipReq.Tag = req.Tag + eipReq.Region = req.Region + eipReq.ProjectId = req.ProjectId + eipResp, err := base.BizClient.AllocateEIP(eipReq) + + if err != nil { + base.HandleError(err) + return + } + + for _, eip := range eipResp.EIPSet { + base.Cxt.Printf("allocate EIP[%s] ", eip.EIPId) + for _, ip := range eip.EIPAddr { + base.Cxt.Printf("IP:%s Line:%s \n", ip.IP, ip.OperatorName) + } + bindEIP(sdk.String(resp.ULBId), sdk.String("ulb"), sdk.String(eip.EIPId), req.ProjectId, req.Region) + } + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + req.ULBName = flags.String("name", "", "Required. Name of ULB instance to create") + flags.StringVar(&mode, "mode", "outer", "Required. Network mode of ULB instance, outer or inner.") + bindRegion(req, flags) + bindProjectID(req, flags) + req.VPCId = flags.String("vpc-id", "", "Optional. Resource ID of VPC which the ULB to create belong to. See 'ucloud vpc list'") + req.SubnetId = flags.String("subnet-id", "", "Optional. Resource ID of subnet. This flag will be discarded when you are creating an outter mode ULB. See 'ucloud subnet list'") + req.ChargeType = flags.String("charge-type", "Month", "Optional.'Year',pay yearly;'Month',pay monthly;'Dynamic', pay hourly") + req.Tag = flags.String("group", "Default", "Optional. Business group") + req.Remark = flags.String("remark", "", "Optional. Remark of instance to create.") + bindEipID = flags.String("bind-eip", "", "Optional. Resource ID or IP Address of eip that will be bound to the new created outer mode ulb") + eipReq.Bandwidth = cmd.Flags().Int("create-eip-bandwidth-mb", 0, "Optional. Required if you want to create new EIP. Bandwidth(Unit:Mbps).The range of value related to network charge mode. By traffic [1, 300]; by bandwidth [1,800] (Unit: Mbps); it could be 0 if the eip belong to the shared bandwidth") + eipReq.OperatorName = flags.String("create-eip-line", "", "Optional. Line of created eip to bind with the new created outer mode ulb") + eipReq.PayMode = cmd.Flags().String("create-eip-traffic-mode", "Bandwidth", "Optional. 'Traffic','Bandwidth' or 'ShareBandwidth'") + eipReq.Name = flags.String("create-eip-name", "", "Optional. Name of created eip to bind with the new created outer mode ulb") + eipReq.Remark = cmd.Flags().String("create-eip-remark", "", "Optional. Remark of your EIP.") + + flags.SetFlagValues("mode", "outer", "inner") + flags.SetFlagValues("charge-type", "Month", "Year", "Dynamic") + flags.SetFlagValues("create-eip-line", "BGP", "International") + flags.SetFlagValues("create-eip-traffic-mode", "Bandwidth", "Traffic", "ShareBandwidth") + flags.SetFlagValuesFunc("bind-eip", func() []string { + return getAllEip(*req.ProjectId, *req.Region, []string{status.EIP_FREE}, nil) + }) + flags.SetFlagValuesFunc("vpc-id", func() []string { + return getAllVPCIdNames(*req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("subnet-id", func() []string { + return getAllSubnetIDNames(*req.VPCId, *req.ProjectId, *req.Region) + }) + + cmd.MarkFlagRequired("mode") + cmd.MarkFlagRequired("name") + + return cmd +} + +// NewCmdULBDelete ucloud ulb delete +func NewCmdULBDelete(out io.Writer) *cobra.Command { + idNames := []string{} + req := base.BizClient.NewDeleteULBRequest() + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete ULB instances by resource ID", + Long: "Delete ULB instances by resource ID", + Run: func(c *cobra.Command, args []string) { + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + for _, idname := range idNames { + req.ULBId = sdk.String(base.PickResourceID(idname)) + _, err := base.BizClient.DeleteULB(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintf(out, "ulb[%s] deleted\n", idname) + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&idNames, "ulb-id", nil, "Required. Resource ID of the ULB instances to delete") + bindRegion(req, flags) + bindProjectID(req, flags) + + flags.SetFlagValuesFunc("ulb-id", func() []string { + return getAllULBIDNames(*req.ProjectId, *req.Region) + }) + + cmd.MarkFlagRequired("ulb-id") + + return cmd +} + +// NewCmdULBUpdate ucloud ulb update +func NewCmdULBUpdate(out io.Writer) *cobra.Command { + var name, group, remark string + idNames := []string{} + req := base.BizClient.NewUpdateULBAttributeRequest() + cmd := &cobra.Command{ + Use: "update", + Short: "Update ULB instance", + Long: "Update ULB instance", + Run: func(c *cobra.Command, args []string) { + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + for _, idname := range idNames { + req.ULBId = sdk.String(base.PickResourceID(idname)) + if name == "" && group == "" && remark == "" { + fmt.Fprintln(out, "Error, name, remark and group can't be all empty") + return + } + if name != "" { + req.Name = &name + } + if group != "" { + req.Tag = &group + } + if remark != "" { + req.Remark = &remark + } + _, err := base.BizClient.UpdateULBAttribute(req) + if err != nil { + base.HandleError(err) + continue + } + fmt.Fprintf(out, "ulb[%s] updated\n", *req.ULBId) + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + bindRegion(req, flags) + bindProjectID(req, flags) + flags.StringSliceVar(&idNames, "ulb-id", nil, "Required. Resource ID of ULB instances to update") + flags.StringVar(&name, "name", "", "Optional, Name of ULB instance") + flags.StringVar(&remark, "remark", "", "Optional, Remark of ULB instance") + flags.StringVar(&group, "group", "", "Optional, Business group of ULB instance") + // bindGroup(&group, flags) + + flags.SetFlagValuesFunc("ulb-id", func() []string { + return getAllULBIDNames(*req.ProjectId, *req.Region) + }) + + cmd.MarkFlagRequired("ulb-id") + + return cmd +} + +func getAllULB(project, region string) ([]ulb.ULBSet, error) { + list := []ulb.ULBSet{} + req := base.BizClient.NewDescribeULBRequest() + req.ProjectId = &project + req.Region = ®ion + + for offset, limit := 0, 50; ; offset += limit { + req.Offset = sdk.Int(offset) + req.Limit = sdk.Int(limit) + resp, err := base.BizClient.DescribeULB(req) + + if err != nil { + return nil, err + } + list = append(list, resp.DataSet...) + + if resp.TotalCount < offset+limit { + break + } + } + return list, nil +} + +func getAllULBIDNames(project, region string) []string { + list := []string{} + ulbList, err := getAllULB(project, region) + if err != nil { + return nil + } + for _, ulb := range ulbList { + list = append(list, fmt.Sprintf("%s/%s", ulb.ULBId, ulb.Name)) + } + return list +} + +// NewCmdULBVserver ucloud ulb-vserver +func NewCmdULBVserver() *cobra.Command { + cmd := &cobra.Command{ + Use: "vserver", + Short: "List and manipulate ULB Vserver instances", + Long: "List and manipulate ULB Vserver instances", + } + out := base.Cxt.GetWriter() + + cmd.AddCommand(NewCmdULBVServerList(out)) + cmd.AddCommand(NewCmdULBVServerCreate(out)) + cmd.AddCommand(NewCmdULBVServerUpdate(out)) + cmd.AddCommand(NewCmdULBVServerDelete(out)) + cmd.AddCommand(NewCmdULBVServerNode()) + cmd.AddCommand(NewCmdULBVServerPolicy()) + + return cmd +} + +// ULBVServerRow 表格行 +type ULBVServerRow struct { + VServerName string + ResourceID string + ListenType string + Protocol string + Port int + LBMethod string + SessionMaintainMode string + SessionMaintainKey string + ClientTimeout string + HealthCheckMode string + HealthCheckDomain string + HealthCheckPath string +} + +// NewCmdULBVServerList ucloud ulb-vserver list +func NewCmdULBVServerList(out io.Writer) *cobra.Command { + req := base.BizClient.NewDescribeVServerRequest() + cmd := &cobra.Command{ + Use: "list", + Short: "List ULB Vserver instances", + Long: "List ULB Vserver instances", + Run: func(c *cobra.Command, args []string) { + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + req.ULBId = sdk.String(base.PickResourceID(*req.ULBId)) + resp, err := base.BizClient.DescribeVServer(req) + if err != nil { + base.HandleError(err) + return + } + list := []ULBVServerRow{} + for _, vs := range resp.DataSet { + row := ULBVServerRow{} + row.VServerName = vs.VServerName + row.ResourceID = vs.VServerId + row.ListenType = vs.ListenType + row.Protocol = vs.Protocol + row.Port = vs.FrontendPort + row.LBMethod = vs.Method + row.ClientTimeout = fmt.Sprintf("%ds", vs.ClientTimeout) + row.SessionMaintainMode = vs.PersistenceType + row.SessionMaintainKey = vs.PersistenceInfo + row.HealthCheckMode = vs.MonitorType + row.HealthCheckDomain = vs.Domain + row.HealthCheckPath = vs.Path + list = append(list, row) + } + base.PrintList(list, out) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + bindRegion(req, flags) + bindProjectID(req, flags) + req.ULBId = flags.String("ulb-id", "", "Required. Resource ID of ULB") + req.VServerId = flags.String("vserver-id", "", "Optional. Resource ID of vserver to list") + + flags.SetFlagValuesFunc("ulb-id", func() []string { + return getAllULBIDNames(*req.ProjectId, *req.Region) + }) + + cmd.MarkFlagRequired("ulb-id") + + return cmd +} + +// NewCmdULBVServerCreate ucloud ulb-vserver create +func NewCmdULBVServerCreate(out io.Writer) *cobra.Command { + sslID := "" + req := base.BizClient.NewCreateVServerRequest() + cmd := &cobra.Command{ + Use: "create", + Short: "Create ULB VServer instance", + Long: "Create ULB VServer instance", + Run: func(c *cobra.Command, args []string) { + if *req.ListenType == "RequestProxy" && (*req.ClientTimeout <= 0 || *req.ClientTimeout > 86400) { + fmt.Println("Error, client-timeout-seconds in the range of (0,86400]") + return + } + if *req.ListenType == "PacketsTransmit" && (*req.ClientTimeout <= 0 || *req.ClientTimeout > 86400) { + fmt.Println("Error, client-timeout-seconds in the range of [60,900]") + return + } + if *req.Protocol == "HTTPS" && sslID == "" { + fmt.Println("Error, SSL Certificate is needed when you choose HTTPS") + return + } + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + req.ULBId = sdk.String(base.PickResourceID(*req.ULBId)) + resp, err := base.BizClient.CreateVServer(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintf(out, "ulb-vserver[%s] created\n", resp.VServerId) + if *req.Protocol == "HTTPS" && sslID != "" { + bindReq := base.BizClient.NewBindSSLRequest() + bindReq.Region = req.Region + bindReq.ProjectId = req.ProjectId + bindReq.SSLId = sdk.String(base.PickResourceID(sslID)) + bindReq.VServerId = sdk.String(resp.VServerId) + bindReq.ULBId = req.ULBId + _, err := base.BizClient.BindSSL(bindReq) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintf(out, "ssl certificate[%s] bind with vserver[%s] of ulb[%s]\n", sslID, *bindReq.VServerId, *bindReq.ULBId) + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + req.ULBId = flags.String("ulb-id", "", "Required. Resource ID of ULB instance which the VServer to create belongs to") + bindRegion(req, flags) + bindProjectID(req, flags) + req.VServerName = flags.String("name", "", "Optional. Name of VServer to create") + req.ListenType = flags.String("listen-type", "RequestProxy", "Optional. Listen type, 'RequestProxy' or 'PacketsTransmit'") + req.Protocol = flags.String("protocol", "HTTP", "Optional. Protocol of VServer instance, 'HTTP','HTTPS','TCP' for listen type 'RequestProxy' and 'TCP','UDP' for listen type 'PacketsTransmit'") + req.FrontendPort = flags.Int("port", 80, "Optional. Port of VServer instance") + flags.StringVar(&sslID, "ssl-id", "", "Optional. Required if you choose HTTPS, Resource ID of SSL Certificate") + req.Method = flags.String("lb-method", "Roundrobin", "Optional. LB methods, accept values:Roundrobin,Source,ConsistentHash,SourcePort,ConsistentHashPort,WeightRoundrobin and Leastconn. \nConsistentHash,SourcePort and ConsistentHashPort are effective for listen type PacketsTransmit only;\nLeastconn is effective for listen type RequestProxy only;\nRoundrobin,Source and WeightRoundrobin are effective for both listen types") + req.PersistenceType = flags.String("session-maintain-mode", "None", "Optional. The method of maintaining user's session. Accept values: 'None','ServerInsert' and 'UserDefined'. 'None' meaning don't maintain user's session'; 'ServerInsert' meaning auto create session key; 'UserDefined' meaning specify session key which accpeted by flag seesion-maintain-key by yourself") + req.PersistenceInfo = flags.String("session-maintain-key", "", "Optional. Specify a key for maintaining session") + req.ClientTimeout = flags.Int("client-timeout-seconds", 60, "Optional.Unit seconds. For 'RequestProxy', it's lifetime for idle connections, range (0,86400]. For 'PacketsTransmit', it's the duration of the connection is maintained, range [60,900]") + req.MonitorType = flags.String("health-check-mode", "Port", "Optional. Method of checking real server's status of health. Accept values:'Port','Path'") + req.Domain = flags.String("health-check-domain", "", "Optional. Skip this flag if health-check-mode is assigned Port") + req.Path = flags.String("health-check-path", "", "Optional. Skip this flags if health-check-mode is assigned Port") + + flags.SetFlagValues("listen-type", "RequestProxy", "PacketsTransmit") + flags.SetFlagValues("protocol", "HTTP", "HTTPS", "TCP", "UDP") + flags.SetFlagValuesFunc("lb-method", func() []string { + if *req.ListenType == "RequestProxy" { + return []string{"Roundrobin", "Source", "WeightRoundrobin", "Leastconn"} + } else if *req.ListenType == "PacketsTransmit" { + return []string{"Roundrobin", "Source", "WeightRoundrobin", "ConsistentHash", "SourcePort", "ConsistentHashPort"} + } + return []string{"Roundrobin", "Source", "WeightRoundrobin", "ConsistentHash", "SourcePort", "ConsistentHashPort", "Leastconn"} + }) + flags.SetFlagValues("session-maintain-mode", "None", "ServerInsert", "UserDefined") + flags.SetFlagValues("health-check-mode", "Port", "Path") + flags.SetFlagValuesFunc("ulb-id", func() []string { + return getAllULBIDNames(*req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("ssl-id", func() []string { + return getAllSSLCertIDNames(*req.ProjectId, *req.Region) + }) + + cmd.MarkFlagRequired("ulb-id") + + return cmd +} + +// NewCmdULBVServerUpdate ucloud ulb-vserver update +func NewCmdULBVServerUpdate(out io.Writer) *cobra.Command { + req := base.BizClient.NewUpdateVServerAttributeRequest() + vserverIDs := []string{} + cmd := &cobra.Command{ + Use: "update", + Short: "Update attributes of VServer instances", + Long: "Update attributes of VServer instances", + Run: func(c *cobra.Command, args []string) { + if *req.VServerName == "" { + req.VServerName = nil + } + if *req.Method == "" { + req.Method = nil + } + if *req.PersistenceType == "" { + req.PersistenceType = nil + } + if *req.PersistenceInfo == "" { + req.PersistenceInfo = nil + } + if *req.ClientTimeout == -1 { + req.ClientTimeout = nil + } + if *req.MonitorType == "" { + req.MonitorType = nil + } + if *req.Domain == "" { + req.Domain = nil + } + if *req.Path == "" { + req.Path = nil + } + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + req.ULBId = sdk.String(base.PickResourceID(*req.ULBId)) + for _, idname := range vserverIDs { + req.VServerId = sdk.String(base.PickResourceID(idname)) + _, err := base.BizClient.UpdateVServerAttribute(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintf(out, "ulb-vserver[%s] updated\n", *req.VServerId) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + req.ULBId = flags.String("ulb-id", "", "Required. Resource ID of ULB instance which the VServer to create belongs to") + flags.StringSliceVar(&vserverIDs, "vserver-id", nil, "Required. Resource ID of Vserver to update") + bindRegion(req, flags) + bindProjectID(req, flags) + req.VServerName = flags.String("name", "", "Optional. Name of VServer") + req.Method = flags.String("lb-method", "", "Optional. LB methods, accept values:Roundrobin,Source,ConsistentHash,SourcePort,ConsistentHashPort,WeightRoundrobin and Leastconn. \nConsistentHash,SourcePort and ConsistentHashPort are effective for listen type PacketsTransmit only;\nLeastconn is effective for listen type RequestProxy only;\nRoundrobin,Source and WeightRoundrobin are effective for both listen types") + req.PersistenceType = flags.String("session-maintain-mode", "", "Optional. The method of maintaining user's session. Accept values: 'None','ServerInsert' and 'UserDefined'. 'None' meaning don't maintain user's session'; 'ServerInsert' meaning auto create session key; 'UserDefined' meaning specify session key which accpeted by flag seesion-maintain-key by yourself") + req.PersistenceInfo = flags.String("session-maintain-key", "", "Optional. Specify a key for maintaining session") + req.ClientTimeout = flags.Int("client-timeout-seconds", -1, "Optional.Unit seconds. For 'RequestProxy', it's lifetime for idle connections, range (0,86400]. For 'PacketsTransmit', it's the duration of the connection is maintained, range [60,900]") + req.MonitorType = flags.String("health-check-mode", "", "Optional. Method of checking real server's status of health. Accept values:'Port','Path'") + req.Domain = flags.String("health-check-domain", "", "Optional. Skip this flag if health-check-mode is assigned Port") + req.Path = flags.String("health-check-path", "", "Optional. Skip this flags if health-check-mode is assigned Port") + + flags.SetFlagValues("lb-method", "Roundrobin", "Source", "WeightRoundrobin", "ConsistentHash", "SourcePort", "ConsistentHashPort", "Leastconn") + flags.SetFlagValues("session-maintain-mode", "None", "ServerInsert", "UserDefined") + flags.SetFlagValues("health-check-mode", "Port", "Path") + flags.SetFlagValuesFunc("ulb-id", func() []string { + return getAllULBIDNames(*req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("vserver-id", func() []string { + ulbID := base.PickResourceID(*req.ULBId) + return getAllULBVServerIDNames(ulbID, *req.ProjectId, *req.Region) + }) + + cmd.MarkFlagRequired("ulb-id") + cmd.MarkFlagRequired("vserver-id") + + return cmd +} + +// NewCmdULBVServerDelete ucloud ulb-vserver delete +func NewCmdULBVServerDelete(out io.Writer) *cobra.Command { + vserverIDs := []string{} + req := base.BizClient.NewDeleteVServerRequest() + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete ULB VServer instances", + Long: "Delete ULB VServer instances", + Run: func(c *cobra.Command, args []string) { + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + req.ULBId = sdk.String(base.PickResourceID(*req.ULBId)) + for _, idname := range vserverIDs { + vsid := base.PickResourceID(idname) + req.VServerId = sdk.String(vsid) + _, err := base.BizClient.DeleteVServer(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintf(out, "ulb-vserver[%s] deleted\n", idname) + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + req.ULBId = flags.String("ulb-id", "", "Required. Resource ID of ULB instance which the VServer to create belongs to") + flags.StringSliceVar(&vserverIDs, "vserver-id", nil, "Required. Resource ID of Vserver to update") + bindRegion(req, flags) + bindProjectID(req, flags) + + cmd.MarkFlagRequired("ulb-id") + cmd.MarkFlagRequired("vserver-id") + + flags.SetFlagValuesFunc("ulb-id", func() []string { + return getAllULBIDNames(*req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("vserver-id", func() []string { + ulbID := base.PickResourceID(*req.ULBId) + return getAllULBVServerIDNames(ulbID, *req.ProjectId, *req.Region) + }) + + return cmd +} + +// NewCmdULBVServerNode ucloud ulb vserver node +func NewCmdULBVServerNode() *cobra.Command { + out := base.Cxt.GetWriter() + cmd := &cobra.Command{ + Use: "backend", + Short: "List and manipulate VServer backend nodes", + Long: "List and manipulate VServer backend nodes", + } + cmd.AddCommand(NewCmdULBVServerListNode(out)) + cmd.AddCommand(NewCmdULBVServerAddNode(out)) + cmd.AddCommand(NewCmdULBVServerUpdateNode(out)) + cmd.AddCommand(NewCmdULBVServerDeleteNode(out)) + return cmd +} + +// ULBVServerNode 表格行 +type ULBVServerNode struct { + Name string + ResourceID string + BackendID string + PrivateIP string + Port int + HealthCheck string + NodeMode string + Weight int +} + +// NewCmdULBVServerListNode ucloud ulb-vserver list-node +func NewCmdULBVServerListNode(out io.Writer) *cobra.Command { + req := base.BizClient.NewDescribeVServerRequest() + cmd := &cobra.Command{ + Use: "list", + Short: "List ULB VServer backend nodes", + Long: "List ULB VServer backend nodes", + Run: func(c *cobra.Command, args []string) { + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + req.ULBId = sdk.String(base.PickResourceID(*req.ULBId)) + req.VServerId = sdk.String(base.PickResourceID(*req.VServerId)) + resp, err := base.BizClient.DescribeVServer(req) + if err != nil { + base.HandleError(err) + return + } + if len(resp.DataSet) != 1 { + fmt.Fprintf(out, "ulb[%s] or vserver[%s] may not exist\n", *req.ULBId, *req.VServerId) + return + } + vs := resp.DataSet[0] + list := []ULBVServerNode{} + for _, node := range vs.BackendSet { + row := ULBVServerNode{} + row.Name = node.ResourceName + row.ResourceID = node.ResourceId + row.BackendID = node.BackendId + row.PrivateIP = node.PrivateIP + row.Weight = node.Weight + row.Port = node.Port + if node.Status == 0 { + row.HealthCheck = "Normal" + } else if node.Status == 1 { + row.HealthCheck = "Failed" + } + if node.Enabled == 1 { + row.NodeMode = "enable" + } else if node.Enabled == 0 { + row.NodeMode = "disable" + } + list = append(list, row) + } + base.PrintList(list, out) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + req.ULBId = flags.String("ulb-id", "", "Required. Resource ID of ULB which the backend nodes belong to") + req.VServerId = flags.String("vserver-id", "", "Required. Resource ID of VServer which the backend nodes belong to") + bindRegion(req, flags) + bindProjectID(req, flags) + + cmd.MarkFlagRequired("ulb-id") + cmd.MarkFlagRequired("vserver-id") + + flags.SetFlagValuesFunc("ulb-id", func() []string { + return getAllULBIDNames(*req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("vserver-id", func() []string { + ulbID := base.PickResourceID(*req.ULBId) + return getAllULBVServerIDNames(ulbID, *req.ProjectId, *req.Region) + }) + + return cmd +} + +// NewCmdULBVServerAddNode ucloud ulb-vserver add-node +func NewCmdULBVServerAddNode(out io.Writer) *cobra.Command { + var enable *string + var weight *int + var ids []string + req := base.BizClient.NewAllocateBackendRequest() + cmd := &cobra.Command{ + Use: "add", + Short: "Add backend nodes for ULB Vserver instance", + Long: "Add backend nodes for ULB Vserver instance", + Run: func(c *cobra.Command, args []string) { + if *enable == "enable" { + req.Enabled = sdk.Int(1) + } else if *enable == "disable" { + req.Enabled = sdk.Int(0) + } else { + fmt.Fprintln(out, "Error, backend-mode must be enable or disable") + return + } + if *weight < 0 || *weight > 100 { + fmt.Fprintln(out, "Error, weight must be between 0 and 100") + return + } + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + req.ULBId = sdk.String(base.PickResourceID(*req.ULBId)) + req.VServerId = sdk.String(base.PickResourceID(*req.VServerId)) + for _, id := range ids { + req.ResourceId = sdk.String(id) + resp, err := base.BizClient.AllocateBackend(req) + if err != nil { + base.HandleError(err) + continue + } + fmt.Fprintf(out, "backend node[%s] added, backend-id:%s\n", *req.ResourceId, resp.BackendId) + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + req.ULBId = flags.String("ulb-id", "", "Required. Resource ID of ULB which the backend nodes belong to") + req.VServerId = flags.String("vserver-id", "", "Required. Resource ID of VServer which the backend nodes belong to") + flags.StringSliceVar(&ids, "resource-id", nil, "Required. Resource ID of the backend nodes to add") + bindRegion(req, flags) + bindProjectID(req, flags) + req.ResourceType = flags.String("resource-type", "UHost", "Optional. Resource type of the backend node to add. Accept values: UHost,UPM,UDHost,UDocker") + req.Port = flags.Int("port", 80, "Optional. The port of your real server on the backend node listening on") + enable = flags.String("backend-mode", "enable", "Optional. Enable backend node or not. Accept values: enable, disable") + weight = flags.Int("weight", 1, "Optional. effective for lb-method WeightRoundrobin. Rnage [0,100]") + + flags.SetFlagValues("resource-type", "Uhost", "UPM", "UDHost", "UDocker") + flags.SetFlagValues("backend-mode", "enable", "disable") + flags.SetFlagValuesFunc("ulb-id", func() []string { + return getAllULBIDNames(*req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("vserver-id", func() []string { + ulbID := base.PickResourceID(*req.ULBId) + return getAllULBVServerIDNames(ulbID, *req.ProjectId, *req.Region) + }) + + cmd.MarkFlagRequired("ulb-id") + cmd.MarkFlagRequired("vserver-id") + cmd.MarkFlagRequired("resource-id") + return cmd +} + +// NewCmdULBVServerUpdateNode ucloud ulb-vserver update-node +func NewCmdULBVServerUpdateNode(out io.Writer) *cobra.Command { + var mode *string + var weight *int + backendIDs := []string{} + req := base.BizClient.NewUpdateBackendAttributeRequest() + cmd := &cobra.Command{ + Use: "update", + Short: "Update attributes of ULB backend nodes", + Long: "Update attributes of ULB backend nodes", + Run: func(c *cobra.Command, args []string) { + if *mode == "enable" { + req.Enabled = sdk.Int(1) + } else if *mode == "disable" { + req.Enabled = sdk.Int(0) + } else if *mode == "" { + req.Enabled = nil + } else { + fmt.Fprintln(out, "Error, backend-mode must be enable or disable") + return + } + if *weight != -1 && (*weight < 0 || *weight > 100) { + fmt.Fprintln(out, "Error, weight must be between 0 and 100") + return + } + if *weight != -1 { + req.Weight = weight + } + + if *req.Port == 0 { + req.Port = nil + } + req.ULBId = sdk.String(base.PickResourceID(*req.ULBId)) + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + for _, bid := range backendIDs { + req.BackendId = sdk.String(base.PickResourceID(bid)) + _, err := base.BizClient.UpdateBackendAttribute(req) + if err != nil { + base.HandleError(err) + continue + } + fmt.Fprintf(out, "backend node[%s] updated\n", bid) + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + req.ULBId = flags.String("ulb-id", "", "Required. Resource ID of ULB which the backend nodes belong to") + flags.StringSliceVar(&backendIDs, "backend-id", nil, "Required. BackendID of backend nodes to update") + req.Port = flags.Int("port", 0, "Optional. Port of your real server listening on backend nodes to update. Rnage [1,65535]") + mode = flags.String("backend-mode", "", "Optional. Enable backend node or not. Accept values: enable, disable") + weight = flags.Int("weight", -1, "Optional. effective for lb-method WeightRoundrobin. Rnage [0,100], -1 meaning no update") + + bindRegion(req, flags) + bindProjectID(req, flags) + + flags.SetFlagValues("backend-mode", "enable", "disable") + flags.SetFlagValuesFunc("ulb-id", func() []string { + return getAllULBIDNames(*req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("backend-id", func() []string { + return getAllULBVServerNodeIDNames(*req.ULBId, "", *req.ProjectId, *req.Region) + }) + + cmd.MarkFlagRequired("ulb-id") + cmd.MarkFlagRequired("backend-id") + + return cmd +} + +// NewCmdULBVServerDeleteNode ucloud ulb-vserver delete-node +func NewCmdULBVServerDeleteNode(out io.Writer) *cobra.Command { + backendIDs := []string{} + req := base.BizClient.NewReleaseBackendRequest() + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete ULB VServer backend nodes", + Long: "Delete ULB VServer backend nodes", + Run: func(c *cobra.Command, args []string) { + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + req.ULBId = sdk.String(base.PickResourceID(*req.ULBId)) + for _, idname := range backendIDs { + req.BackendId = sdk.String(base.PickResourceID(idname)) + _, err := base.BizClient.ReleaseBackend(req) + if err != nil { + base.HandleError(err) + continue + } + fmt.Fprintf(out, "backend node[%s] deleted\n", idname) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + req.ULBId = flags.String("ulb-id", "", "Required. Resource ID of ULB which the backend nodes belong to") + flags.StringSliceVar(&backendIDs, "backend-id", nil, "Required. BackendID of backend nodes to update") + bindRegion(req, flags) + bindProjectID(req, flags) + + cmd.MarkFlagRequired("ulb-id") + cmd.MarkFlagRequired("backend-id") + + flags.SetFlagValuesFunc("ulb-id", func() []string { + return getAllULBIDNames(*req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("backend-id", func() []string { + return getAllULBVServerNodeIDNames(*req.ULBId, "", *req.ProjectId, *req.Region) + }) + return cmd +} + +// NewCmdULBVServerPolicy ucloud ulb vserver policy +func NewCmdULBVServerPolicy() *cobra.Command { + out := base.Cxt.GetWriter() + cmd := &cobra.Command{ + Use: "policy", + Short: "List and manipulate forward policy for VServer", + Long: "List and manipulate forward policy for VServer", + } + cmd.AddCommand(NewCmdULBVServerCreatePolicy(out)) + cmd.AddCommand(NewCmdULBVServerListPolicy(out)) + cmd.AddCommand(NewCmdULBVServerUpdatePolicy(out)) + cmd.AddCommand(NewCmdULBVServerDeletePolicy(out)) + return cmd +} + +// NewCmdULBVServerCreatePolicy ucloud ulb-vserver create-policy +func NewCmdULBVServerCreatePolicy(out io.Writer) *cobra.Command { + backendIDs := []string{} + req := base.BizClient.NewCreatePolicyRequest() + cmd := &cobra.Command{ + Use: "add", + Short: "Add content forward policy for VServer", + Long: "Add content forward policy for VServer", + Run: func(c *cobra.Command, args []string) { + if *req.Type != "Domain" && *req.Type != "Path" { + fmt.Fprintln(out, "Error, forward method must be Domain or Path") + return + } + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + req.ULBId = sdk.String(base.PickResourceID(*req.ULBId)) + req.VServerId = sdk.String(base.PickResourceID(*req.VServerId)) + for _, idname := range backendIDs { + req.BackendId = append(req.BackendId, base.PickResourceID(idname)) + } + resp, err := base.BizClient.CreatePolicy(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintf(out, "policy[%s] created\n", resp.PolicyId) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + req.ULBId = flags.String("ulb-id", "", "Required. Resource ID of ULB") + req.VServerId = flags.String("vserver-id", "", "Required. Resource ID of VServer") + flags.StringSliceVar(&backendIDs, "backend-id", nil, "Required. BackendID of the VServer's backend nodes") + req.Type = flags.String("forward-method", "", "Required. Forward method, accept values:Domain and Path; Both forwarding methods can be described by using regular expressions or wildcards") + req.Match = flags.String("expression", "", "Required. Expression of domain or path, such as \"www.[123].demo.com\" or \"/path/img/*.jpg\"") + bindRegion(req, flags) + bindProjectID(req, flags) + + flags.SetFlagValues("forward-method", "Domain", "Path") + flags.SetFlagValuesFunc("ulb-id", func() []string { + return getAllULBIDNames(*req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("vserver-id", func() []string { + return getAllULBVServerIDNames(*req.ULBId, *req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("backend-id", func() []string { + return getAllULBVServerNodeIDNames(*req.ULBId, *req.VServerId, *req.ProjectId, *req.Region) + }) + + cmd.MarkFlagRequired("ulb-id") + cmd.MarkFlagRequired("vserver-id") + cmd.MarkFlagRequired("backend-id") + cmd.MarkFlagRequired("forward-method") + cmd.MarkFlagRequired("expression") + + return cmd +} + +// ULBVServerPolicy 表格行 +type ULBVServerPolicy struct { + ForwardMethod string + Expression string + PolicyID string + PolicyType string + Backends string +} + +// NewCmdULBVServerListPolicy ucloud ulb-vserver list-policy +func NewCmdULBVServerListPolicy(out io.Writer) *cobra.Command { + var ulbID, vserverID *string + region := base.ConfigIns.Region + project := base.ConfigIns.ProjectID + cmd := &cobra.Command{ + Use: "list", + Short: "List content forward policies of the VServer instance", + Long: "List content forward policies of the VServer instance", + Run: func(c *cobra.Command, args []string) { + ulbID = sdk.String(base.PickResourceID(*ulbID)) + vserverID = sdk.String(base.PickResourceID(*vserverID)) + vsList, err := getAllULBVServer(*ulbID, *vserverID, project, region) + if err != nil { + base.HandleError(err) + return + } + if len(vsList) == 1 { + vs := vsList[0] + list := []ULBVServerPolicy{} + for _, p := range vs.PolicySet { + row := ULBVServerPolicy{} + row.ForwardMethod = p.Type + row.Expression = p.Match + row.PolicyID = p.PolicyId + row.PolicyType = p.PolicyType + nodes := []string{} + for _, b := range p.BackendSet { + nodes = append(nodes, fmt.Sprintf("%s|%s:%d|%s", b.BackendId, b.PrivateIP, b.Port, b.ResourceName)) + } + row.Backends = strings.Join(nodes, ",") + list = append(list, row) + } + base.PrintList(list, out) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + bindRegionS(®ion, flags) + bindProjectIDS(&project, flags) + + ulbID = flags.String("ulb-id", "", "Required. Resource ID of ULB") + vserverID = flags.String("vserver-id", "", "Required. Resource ID of VServer") + + flags.SetFlagValuesFunc("ulb-id", func() []string { + return getAllULBIDNames(project, region) + }) + flags.SetFlagValuesFunc("vserver-id", func() []string { + ulb := base.PickResourceID(*ulbID) + return getAllULBVServerIDNames(ulb, project, region) + }) + cmd.MarkFlagRequired("ulb-id") + cmd.MarkFlagRequired("vserver-id") + return cmd +} + +// NewCmdULBVServerUpdatePolicy ucloud ulb-vserver update-policy +func NewCmdULBVServerUpdatePolicy(out io.Writer) *cobra.Command { + policyIDs := []string{} + backendIDs := []string{} + addBackendIDs := []string{} + removeBackendIDs := []string{} + req := base.BizClient.NewUpdatePolicyRequest() + cmd := &cobra.Command{ + Use: "update", + Short: "Update content forward policies of ULB VServer", + Long: "Update content forward policies ULB VServer", + Run: func(c *cobra.Command, args []string) { + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + req.ULBId = sdk.String(base.PickResourceID(*req.ULBId)) + req.VServerId = sdk.String(base.PickResourceID(*req.VServerId)) + + vsList, err := getAllULBVServer(*req.ULBId, *req.VServerId, *req.ProjectId, *req.Region) + if err != nil { + base.HandleError(err) + return + } + vs := vsList[0] + + for _, policyID := range policyIDs { + var policy *ulb.ULBPolicySet + for _, p := range vs.PolicySet { + if p.PolicyId == policyID { + policy = &p + break + } + } + if policy == nil { + fmt.Fprintf(out, "policy[%s] not found\n", *req.PolicyId) + continue + } + req.PolicyId = sdk.String(policyID) + if *req.Type == "" { + req.Type = sdk.String(policy.Type) + } else if *req.Type != "Domain" && *req.Type != "Path" { + fmt.Fprintf(out, "Error, forward-method must be Domain or Path") + continue + } + if *req.Match == "" { + req.Match = sdk.String(policy.Match) + } + backendIDMap := map[string]bool{} + if backendIDs == nil { + for _, b := range policy.BackendSet { + backendIDMap[b.BackendId] = true + } + } else { + for _, bid := range backendIDs { + backendIDMap[base.PickResourceID(bid)] = true + } + } + for _, bid := range addBackendIDs { + backendIDMap[base.PickResourceID(bid)] = true + } + for _, bid := range removeBackendIDs { + backendIDMap[base.PickResourceID(bid)] = false + } + for bid, ok := range backendIDMap { + if ok { + req.BackendId = append(req.BackendId, bid) + } + } + resp, err := base.BizClient.UpdatePolicy(req) + if err != nil { + base.HandleError(err) + continue + } + fmt.Fprintf(out, "policy[%s] updated\n", resp.PolicyId) + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + bindRegion(req, flags) + bindProjectID(req, flags) + req.ULBId = flags.String("ulb-id", "", "Required. Resource ID of ULB") + req.VServerId = flags.String("vserver-id", "", "Required. Resource ID of VServer") + flags.StringSliceVar(&policyIDs, "policy-id", nil, "Required. PolicyID of policies to update") + flags.StringSliceVar(&backendIDs, "backend-id", nil, "Optional. BackendID of backend nodes. If assign this flag, it will rewrite all backend nodes of the policy") + flags.StringSliceVar(&addBackendIDs, "add-backend-id", nil, "Optional. BackendID of backend nodes. Add backend nodes to the policy") + flags.StringSliceVar(&removeBackendIDs, "remove-backend-id", nil, "Optional. BackendID of backend nodes. Remove those backend nodes from the policy") + req.Type = flags.String("forward-method", "", "Optional. Forward method of policy, accept values:Domain and Path") + req.Match = flags.String("expression", "", "Optional. Expression of domain or path, such as \"www.[123].demo.com\" or \"/path/img/*.jpg\"") + + cmd.MarkFlagRequired("ulb-id") + cmd.MarkFlagRequired("vserver-id") + cmd.MarkFlagRequired("policy-id") + + flags.SetFlagValues("forward-method", "Domain", "Path") + flags.SetFlagValuesFunc("ulb-id", func() []string { + project := base.PickResourceID(*req.ProjectId) + return getAllULBIDNames(project, *req.Region) + }) + flags.SetFlagValuesFunc("vserver-id", func() []string { + return getAllULBVServerIDNames(*req.ULBId, *req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("backend-id", func() []string { + return getAllULBVServerNodeIDNames(*req.ULBId, *req.VServerId, *req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("add-backend-id", func() []string { + return getAllULBVServerNodeIDNames(*req.ULBId, *req.VServerId, *req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("remove-backend-id", func() []string { + return getAllULBVServerNodeIDNames(*req.ULBId, *req.VServerId, *req.ProjectId, *req.Region) + }) + + return cmd +} + +// NewCmdULBVServerDeletePolicy ucloud ulb-vserver delete-policy +func NewCmdULBVServerDeletePolicy(out io.Writer) *cobra.Command { + policyIDs := []string{} + req := base.BizClient.NewDeletePolicyRequest() + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete content forward policies of ULB VServer", + Long: "Delete content forward policies of ULB VServer", + Run: func(c *cobra.Command, args []string) { + for _, p := range policyIDs { + req.PolicyId = sdk.String(p) + _, err := base.BizClient.DeletePolicy(req) + if err != nil { + base.HandleError(err) + continue + } + fmt.Fprintf(out, "policy[%s] deleted\n", p) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + bindRegion(req, flags) + bindProjectID(req, flags) + + flags.StringSliceVar(&policyIDs, "policy-id", nil, "Required. PolicyID of policies to delete") + req.VServerId = flags.String("vserver-id", "", "Optional. Resource ID of VServer") + + cmd.MarkFlagRequired("policy-id") + + return cmd +} + +// NewCmdULBSSL ucloud ulb-ssl-certificate +func NewCmdULBSSL() *cobra.Command { + cmd := &cobra.Command{ + Use: "ssl", + Short: "List and manipulate SSL Certificates for ULB", + Long: "List and manipulate SSL Certificates for ULB", + } + out := base.Cxt.GetWriter() + cmd.AddCommand(NewCmdSSLList(out)) + cmd.AddCommand(NewCmdSSLDescribe(out)) + cmd.AddCommand(NewCmdSSLAdd(out)) + cmd.AddCommand(NewCmdSSLDelete(out)) + cmd.AddCommand(NewCmdSSLBind(out)) + cmd.AddCommand(NewCmdSSLUnbind(out)) + return cmd +} + +// SSLCertificate 表格行 +type SSLCertificate struct { + Name string + ResourceID string + MD5 string + BindResource string + UploadTime string +} + +// NewCmdSSLList ucloud ulb-ssl-certificate list +func NewCmdSSLList(out io.Writer) *cobra.Command { + req := base.BizClient.NewDescribeSSLRequest() + cmd := &cobra.Command{ + Use: "list", + Short: "List SSL Certificates", + Long: "List SSL Certificates", + Run: func(c *cobra.Command, args []string) { + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + resp, err := base.BizClient.DescribeSSL(req) + if err != nil { + base.HandleError(err) + return + } + rows := []SSLCertificate{} + for _, ssl := range resp.DataSet { + row := SSLCertificate{} + row.Name = ssl.SSLName + row.ResourceID = ssl.SSLId + row.MD5 = ssl.HashValue + row.UploadTime = base.FormatDateTime(ssl.CreateTime) + targets := []string{} + for _, t := range ssl.BindedTargetSet { + item := fmt.Sprintf("%s/%s(%s/%s)", t.VServerId, t.VServerName, t.ULBId, t.ULBName) + targets = append(targets, item) + } + row.BindResource = strings.Join(targets, ",") + rows = append(rows, row) + } + base.PrintList(rows, out) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + bindRegion(req, flags) + bindProjectID(req, flags) + req.SSLId = flags.String("ssl-id", "", "Optional. ResouceID of ssl certificate to list") + bindLimit(req, flags) + bindOffset(req, flags) + + return cmd +} + +// NewCmdSSLDescribe ucloud ulb-ssl-certificate describe +func NewCmdSSLDescribe(out io.Writer) *cobra.Command { + req := base.BizClient.NewDescribeSSLRequest() + cmd := &cobra.Command{ + Use: "describe", + Short: "Display all data associated with SSL Certificate", + Long: "Display all data associated with SSL Certificate", + Run: func(c *cobra.Command, args []string) { + req.SSLId = sdk.String(base.PickResourceID(*req.SSLId)) + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + resp, err := base.BizClient.DescribeSSL(req) + if err != nil { + base.HandleError(err) + return + } + if len(resp.DataSet) <= 0 { + fmt.Fprintf(out, "ssl certificate[%s] is not exists\n", *req.SSLId) + return + } + + sslcf := resp.DataSet[0] + targets := []string{} + for _, t := range sslcf.BindedTargetSet { + item := fmt.Sprintf("%s/%s-%s/%s", t.ULBId, t.ULBName, t.VServerId, t.VServerName) + targets = append(targets, item) + } + rows := []base.DescribeTableRow{ + { + Attribute: "ResourceID", + Content: sslcf.SSLId, + }, + { + Attribute: "Name", + Content: sslcf.SSLName, + }, + { + Attribute: "Type", + Content: sslcf.SSLType, + }, + { + Attribute: "UploadTime", + Content: base.FormatDateTime(sslcf.CreateTime), + }, + { + Attribute: "BindResource", + Content: strings.Join(targets, ","), + }, + { + Attribute: "MD5", + Content: sslcf.HashValue, + }, + { + Attribute: "Content", + Content: sslcf.SSLContent, + }, + } + base.PrintDescribe(rows, global.JSON) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + req.SSLId = flags.String("ssl-id", "", "Required. ResouceID of ssl certificate to describe") + bindRegion(req, flags) + bindProjectID(req, flags) + flags.SetFlagValuesFunc("ssl-id", func() []string { + return getAllSSLCertIDNames(*req.ProjectId, *req.Region) + }) + cmd.MarkFlagRequired("ssl-id") + return cmd +} + +// NewCmdSSLAdd ucloud ulb-ssl-certificate add +func NewCmdSSLAdd(out io.Writer) *cobra.Command { + var allPath, sitePath, keyPath, caPath *string + req := base.BizClient.NewCreateSSLRequest() + cmd := &cobra.Command{ + Use: "add", + Short: "Add SSL Certificate", + Long: "Add SSL Certificate", + Run: func(c *cobra.Command, args []string) { + if *allPath == "" && (*sitePath == "" || *keyPath == "") { + fmt.Fprintln(out, "if all-in-one-file is omitted, site-certificate-file and private-key-file can't be empty") + return + } + if *allPath != "" { + content, err := readFile(*allPath) + if err != nil { + base.HandleError(err) + return + } + req.SSLContent = &content + } + if *sitePath != "" { + content, err := readFile(*sitePath) + if err != nil { + base.HandleError(err) + return + } + req.UserCert = &content + } + if *keyPath != "" { + content, err := readFile(*keyPath) + if err != nil { + base.HandleError(err) + return + } + req.PrivateKey = &content + } + if *caPath != "" { + content, err := readFile(*caPath) + if err != nil { + base.HandleError(err) + return + } + req.CaCert = &content + } + + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + resp, err := base.BizClient.CreateSSL(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintf(out, "ssl certificate[%s] added\n", resp.SSLId) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + bindRegion(req, flags) + bindProjectID(req, flags) + req.SSLName = flags.String("name", "", "Required. Name of ssl certificate to add") + req.SSLType = flags.String("format", "Pem", "Optional. Format of ssl certificate") + allPath = flags.String("all-in-one-file", "", "Optional. Path of file which contain the complete content of the SSL certificate, including the content of site certificate, the private key which encrypted the site certificate, and the CA certificate. ") + sitePath = flags.String("site-certificate-file", "", "Optional. Path of user's certificate file, *.crt. Required if all-in-one-file is omitted") + keyPath = flags.String("private-key-file", "", "Optional. Path of private key file, *.key. Required if all-in-one-file is omitted") + caPath = flags.String("ca-certificate-file", "", "Optional. Path of CA certificate file, *.crt") + cmd.MarkFlagRequired("name") + flags.SetFlagValuesFunc("all-in-one-file", func() []string { + return base.GetFileList("") + }) + flags.SetFlagValuesFunc("private-key-file", func() []string { + return base.GetFileList(".key") + }) + flags.SetFlagValuesFunc("ca-certificate-file", func() []string { + return base.GetFileList(".crt") + }) + flags.SetFlagValuesFunc("site-certificate-file", func() []string { + return base.GetFileList(".crt") + }) + return cmd +} + +// NewCmdSSLDelete ucloud ulb-ssl-certificate delete +func NewCmdSSLDelete(out io.Writer) *cobra.Command { + var idNames []string + req := base.BizClient.NewDeleteSSLRequest() + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete SSL Certificates by resource id(ssl id)", + Long: "Delete SSL Certificates by resource id(ssl id)", + Run: func(c *cobra.Command, args []string) { + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + for _, idname := range idNames { + req.SSLId = sdk.String(base.PickResourceID(idname)) + _, err := base.BizClient.DeleteSSL(req) + if err != nil { + base.HandleError(err) + continue + } + fmt.Fprintf(out, "ssl certificate[%s] deleted\n", idname) + } + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + bindRegion(req, flags) + bindProjectID(req, flags) + flags.StringSliceVar(&idNames, "ssl-id", nil, "Required. Resource ID of SSL Certificates to delete") + flags.SetFlagValuesFunc("ssl-id", func() []string { + return getAllSSLCertIDNames(*req.ProjectId, *req.Region) + }) + return cmd +} + +// NewCmdSSLBind ucloud ulb-ssl-certificate bind +func NewCmdSSLBind(out io.Writer) *cobra.Command { + req := base.BizClient.NewBindSSLRequest() + cmd := &cobra.Command{ + Use: "bind", + Short: "Bind SSL Certificate with VServer", + Long: "Bind SSL Certificate with VServer", + Run: func(c *cobra.Command, args []string) { + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + req.ULBId = sdk.String(base.PickResourceID(*req.ULBId)) + req.VServerId = sdk.String(base.PickResourceID(*req.VServerId)) + req.SSLId = sdk.String(base.PickResourceID(*req.SSLId)) + _, err := base.BizClient.BindSSL(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintf(out, "ssl certificate[%s] bind with vserver[%s] of ulb[%s]\n", *req.SSLId, *req.VServerId, *req.ULBId) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + bindRegion(req, flags) + bindProjectID(req, flags) + req.SSLId = flags.String("ssl-id", "", "Required. Resource ID of SSL Certificate to bind") + req.ULBId = flags.String("ulb-id", "", "Required. Resource ID of ULB") + req.VServerId = flags.String("vserver-id", "", "Required. Resource ID of VServer") + flags.SetFlagValuesFunc("ssl-id", func() []string { + return getAllSSLCertIDNames(*req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("ulb-id", func() []string { + return getAllULBIDNames(*req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("vserver-id", func() []string { + return getAllULBVServerIDNames(*req.ULBId, *req.ProjectId, *req.Region) + }) + cmd.MarkFlagRequired("ssl-id") + cmd.MarkFlagRequired("ulb-id") + cmd.MarkFlagRequired("vserver-id") + return cmd +} + +// NewCmdSSLUnbind ucloud ulb-ssl-certificate unbind +func NewCmdSSLUnbind(out io.Writer) *cobra.Command { + req := base.BizClient.NewUnbindSSLRequest() + cmd := &cobra.Command{ + Use: "unbind", + Short: "Unbind SSL Certificate with VServer", + Long: "Unbind SSL Certificate with VServer", + Run: func(c *cobra.Command, args []string) { + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + req.ULBId = sdk.String(base.PickResourceID(*req.ULBId)) + req.VServerId = sdk.String(base.PickResourceID(*req.VServerId)) + req.SSLId = sdk.String(base.PickResourceID(*req.SSLId)) + _, err := base.BizClient.UnbindSSL(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintf(out, "ssl certificate[%s] unbind with vserver[%s] of ulb[%s]\n", *req.SSLId, *req.VServerId, *req.ULBId) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + bindRegion(req, flags) + bindProjectID(req, flags) + req.SSLId = flags.String("ssl-id", "", "Required. Resource ID of SSL Certificate to unbind") + req.ULBId = flags.String("ulb-id", "", "Required. Resource ID of ULB") + req.VServerId = flags.String("vserver-id", "", "Required. Resource ID of VServer") + flags.SetFlagValuesFunc("ssl-id", func() []string { + return getAllSSLCertIDNames(*req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("ulb-id", func() []string { + if *req.SSLId == "" { + return getAllULBIDNames(*req.ProjectId, *req.Region) + } + cert, err := getSSLCertByID(*req.SSLId, *req.ProjectId, *req.Region) + if err != nil { + return nil + } + ulbs := []string{} + for _, b := range cert.BindedTargetSet { + ulbs = append(ulbs, fmt.Sprintf("%s/%s", b.ULBId, b.ULBName)) + } + return ulbs + }) + flags.SetFlagValuesFunc("vserver-id", func() []string { + if *req.SSLId == "" { + return getAllULBVServerIDNames(*req.ULBId, *req.ProjectId, *req.Region) + } + cert, err := getSSLCertByID(*req.SSLId, *req.ProjectId, *req.Region) + if err != nil { + return nil + } + vservers := []string{} + for _, b := range cert.BindedTargetSet { + vservers = append(vservers, fmt.Sprintf("%s/%s", b.VServerId, b.VServerName)) + } + return vservers + }) + cmd.MarkFlagRequired("ssl-id") + cmd.MarkFlagRequired("ulb-id") + cmd.MarkFlagRequired("vserver-id") + return cmd +} + +func readFile(file string) (string, error) { + byts, err := ioutil.ReadFile(file) + if err != nil { + return "", err + } + return string(byts), nil +} + +func getAllULBVServerNodes(ulbID, vserverID, project, region string) ([]ulb.ULBBackendSet, error) { + vsList, err := getAllULBVServer(ulbID, vserverID, project, region) + if err != nil { + return nil, err + } + nodeList := []ulb.ULBBackendSet{} + for _, vs := range vsList { + nodeList = append(nodeList, vs.BackendSet...) + } + return nodeList, nil +} + +func getAllULBVServerNodeIDNames(ulbID, vserverID, project, region string) []string { + nodeList, err := getAllULBVServerNodes(ulbID, vserverID, project, region) + if err != nil { + return nil + } + idNames := []string{} + for _, node := range nodeList { + idNames = append(idNames, fmt.Sprintf("%s/%s", node.BackendId, node.ResourceName)) + } + return idNames +} + +func getAllSSLCertIDNames(project, region string) []string { + sslcs, err := getAllSSLCerts(project, region) + if err != nil { + return nil + } + idNames := []string{} + for _, ssl := range sslcs { + idNames = append(idNames, fmt.Sprintf("%s/%s", ssl.SSLId, ssl.SSLName)) + } + return idNames +} + +func getAllSSLCerts(project, region string) ([]ulb.ULBSSLSet, error) { + req := base.BizClient.NewDescribeSSLRequest() + req.ProjectId = sdk.String(base.PickResourceID(project)) + req.Region = sdk.String(region) + list := []ulb.ULBSSLSet{} + for offset, limit := 0, 50; ; offset += limit { + req.Offset = sdk.Int(offset) + req.Limit = sdk.Int(limit) + resp, err := base.BizClient.DescribeSSL(req) + if err != nil { + return nil, err + } + list = append(list, resp.DataSet...) + if resp.TotalCount <= offset+limit { + break + } + } + return list, nil +} + +func getSSLCertByID(sslID, project, region string) (*ulb.ULBSSLSet, error) { + if sslID == "" { + return nil, fmt.Errorf("ssl certificate resource id can't be empty") + } + req := base.BizClient.NewDescribeSSLRequest() + req.ProjectId = sdk.String(base.PickResourceID(project)) + req.Region = sdk.String(region) + req.SSLId = sdk.String(base.PickResourceID(sslID)) + resp, err := base.BizClient.DescribeSSL(req) + if err != nil { + return nil, err + } + if len(resp.DataSet) <= 0 { + return nil, fmt.Errorf("ssl certificate[%s] is not exists", sslID) + } + return &resp.DataSet[0], nil +} + +func getAllULBVServer(ulbID, vserverID, project, region string) ([]ulb.ULBVServerSet, error) { + req := base.BizClient.NewDescribeVServerRequest() + req.ULBId = sdk.String(base.PickResourceID(ulbID)) + req.ProjectId = sdk.String(base.PickResourceID(project)) + req.Region = ®ion + if vserverID != "" { + req.VServerId = sdk.String(base.PickResourceID(vserverID)) + } + resp, err := base.BizClient.DescribeVServer(req) + if err != nil { + return nil, err + } + if vserverID != "" { + if len(resp.DataSet) < 1 { + return nil, fmt.Errorf("VServer[%s] may not exist", vserverID) + } else if len(resp.DataSet) > 1 { + return nil, fmt.Errorf("Internal Error, too many vserver:%#v", resp.DataSet) + } + } + return resp.DataSet, nil +} + +func getAllULBVServerIDNames(ulbID, project, region string) []string { + vservers, err := getAllULBVServer(ulbID, "", project, region) + if err != nil { + return nil + } + idNames := []string{} + for _, vs := range vservers { + idNames = append(idNames, fmt.Sprintf("%s/%s", vs.VServerId, vs.VServerName)) + } + return idNames +} diff --git a/cmd/ulhost.go b/cmd/ulhost.go new file mode 100644 index 0000000000..9344e944d5 --- /dev/null +++ b/cmd/ulhost.go @@ -0,0 +1,26 @@ +package cmd + +import ( + "github.com/ucloud/ucloud-sdk-go/ucloud/request" + + "github.com/ucloud/ucloud-cli/base" +) + +var ulhostSpoller = base.NewSpoller(sdescribeULHostByID, base.Cxt.GetWriter()) + +func sdescribeULHostByID(ulhostID string, common *request.CommonBase) (interface{}, error) { + req := base.BizClient.UCompShareClient.NewDescribeULHostInstanceRequest() + req.ULHostIds = []string{ulhostID} + if common != nil { + req.CommonBase = *common + } + resp, err := base.BizClient.UCompShareClient.DescribeULHostInstance(req) + if err != nil { + return nil, err + } + if len(resp.ULHostInstanceSets) < 1 { + return nil, nil + } + + return &resp.ULHostInstanceSets[0], nil +} diff --git a/cmd/umem.go b/cmd/umem.go new file mode 100644 index 0000000000..3964e9af8a --- /dev/null +++ b/cmd/umem.go @@ -0,0 +1,639 @@ +// Copyright © 2018 NAME HERE tony.li@ucloud.cn +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "io" + "strings" + + "github.com/spf13/cobra" + + "github.com/ucloud/ucloud-sdk-go/services/umem" + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + "github.com/ucloud/ucloud-sdk-go/ucloud/request" + + "github.com/ucloud/ucloud-cli/base" + "github.com/ucloud/ucloud-cli/model/status" + "github.com/ucloud/ucloud-cli/ux" +) + +// NewCmdRedis ucloud redis +func NewCmdRedis() *cobra.Command { + cmd := &cobra.Command{ + Use: "redis", + Short: "List and manipulate redis instances", + Long: "List and manipulate redis instances", + } + out := base.Cxt.GetWriter() + cmd.AddCommand(NewCmdRedisList(out)) + cmd.AddCommand(NewCmdRedisCreate(out)) + cmd.AddCommand(NewCmdRedisDelete(out)) + cmd.AddCommand(NewCmdRedisRestart(out)) + return cmd +} + +// NewCmdMemcache ucloud memcache +func NewCmdMemcache() *cobra.Command { + cmd := &cobra.Command{ + Use: "memcache", + Short: "List and manipulate memcache instances", + Long: "List and manipulate memcache instances", + } + out := base.Cxt.GetWriter() + cmd.AddCommand(NewCmdMemcacheList(out)) + cmd.AddCommand(NewCmdMemcacheCreate(out)) + cmd.AddCommand(NewCmdMemcacheDelete(out)) + cmd.AddCommand(NewCmdMemcacheRestart(out)) + return cmd +} + +// UMemRedisRow 表格行 +type UMemRedisRow struct { + ResourceID string + Name string + Role string + Type string + Address string + Size string + UsedSize string + State string + Group string + Zone string + CreateTime string +} + +var redisTypeMap = map[string]string{ + "single": "master-replica", + "distributed": "distributed", +} + +// NewCmdRedisList ucloud redis list +func NewCmdRedisList(out io.Writer) *cobra.Command { + req := base.BizClient.NewDescribeUMemRequest() + cmd := &cobra.Command{ + Use: "list", + Short: "List redis instances", + Long: "List redis instances", + Run: func(c *cobra.Command, args []string) { + resp, err := base.BizClient.DescribeUMem(req) + if err != nil { + base.HandleError(err) + return + } + list := []UMemRedisRow{} + for _, ins := range resp.DataSet { + row := UMemRedisRow{ + ResourceID: ins.ResourceId, + Name: ins.Name, + Role: ins.Role, + Type: redisTypeMap[ins.ResourceType], + Group: ins.Tag, + Size: fmt.Sprintf("%dGB", ins.Size), + UsedSize: fmt.Sprintf("%dMB", ins.UsedSize), + State: ins.State, + Zone: ins.Zone, + CreateTime: base.FormatDate(ins.CreateTime), + } + addrs := []string{} + for _, addr := range ins.Address { + addrs = append(addrs, fmt.Sprintf("%s:%d", addr.IP, addr.Port)) + } + row.Address = strings.Join(addrs, "|") + list = append(list, row) + for _, slave := range ins.DataSet { + srow := UMemRedisRow{ + ResourceID: slave.GroupId, + Name: slave.Name, + Role: fmt.Sprintf("\u2b91 %s", slave.Role), + Type: redisTypeMap[slave.ResourceType], + Group: slave.Tag, + Size: fmt.Sprintf("%dGB", slave.Size), + UsedSize: fmt.Sprintf("%dMB", slave.UsedSize), + State: slave.State, + Zone: slave.Zone, + Address: fmt.Sprintf("%s:%d", slave.VirtualIP, slave.Port), + CreateTime: base.FormatDate(slave.CreateTime), + } + list = append(list, srow) + } + } + base.PrintList(list, out) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + req.ResourceId = flags.String("umem-id", "", "Optional. Resource ID of the redis to list") + bindRegion(req, flags) + bindZoneEmpty(req, flags) + bindProjectID(req, flags) + bindOffset(req, flags) + bindLimit(req, flags) + req.Protocol = sdk.String("redis") + + flags.SetFlagValuesFunc("umem-id", func() []string { + return getRedisIDList(*req.ProjectId, *req.Region) + }) + + return cmd +} + +// NewCmdRedisCreate ucloud redis create +func NewCmdRedisCreate(out io.Writer) *cobra.Command { + req := base.BizClient.NewCreateURedisGroupRequest() + req.HighAvailability = sdk.String("enable") + var redisType, password string + cmd := &cobra.Command{ + Use: "create", + Short: "Create redis instance", + Long: "Create redis instance", + Run: func(c *cobra.Command, args []string) { + if l := len(*req.Name); l < 6 || l > 63 { + fmt.Fprintln(out, "length of name should be between 6 and 63") + return + } + if password != "" { + req.Password = &password + } + if redisType == "master-replica" { + resp, err := base.BizClient.CreateURedisGroup(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Printf("redis[%s] created\n", resp.GroupId) + } else if redisType == "distributed" { + dreq := base.BizClient.NewCreateUMemSpaceRequest() + dreq.Region = req.Region + dreq.Zone = req.Zone + dreq.ProjectId = req.ProjectId + dreq.Name = req.Name + dreq.Size = req.Size + if *req.Size == 1 { + dreq.Size = sdk.Int(16) + } + dreq.ChargeType = req.ChargeType + dreq.Quantity = req.Quantity + dreq.Tag = req.Tag + dreq.Password = req.Password + resp, err := base.BizClient.CreateUMemSpace(dreq) + if err != nil { + base.HandleError(err) + return + } + fmt.Printf("redis[%s] created\n", resp.SpaceId) + } else { + fmt.Printf("unknow redis type[%s], it's should be 'master-replica' or 'distributed'\n", redisType) + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + req.Name = flags.String("name", "", "Required. Name of the redis to create. Range of the password length is [6,63] and the password can only contain letters and numbers") + flags.StringVar(&redisType, "type", "", "Required. Type of the redis. Accept values:'master-replica','distributed'") + req.Size = flags.Int("size-gb", 1, "Optional. Memory size. Default value 1GB(for master-replica redis type) or 16GB(for distributed redis type). Unit GB") + req.Version = flags.String("version", "3.2", "Optional. Version of redis") + req.VPCId = flags.String("vpc-id", "", "Optional. VPC ID. This field is required under VPC2.0. See 'ucloud vpc list'") + req.SubnetId = flags.String("subnet-id", "", "Optional. Subnet ID. This field is required under VPC2.0. See 'ucloud subnet list'") + flags.StringVar(&password, "password", "", "Optional. Password of redis to create") + + bindRegion(req, flags) + bindZone(req, flags) + bindProjectID(req, flags) + bindGroup(req, flags) + bindChargeType(req, flags) + bindQuantity(req, flags) + + flags.SetFlagValues("version", "3.0", "3.2", "4.0") + flags.SetFlagValues("type", "master-replica", "distributed") + flags.SetFlagValuesFunc("vpc-id", func() []string { + return getAllVPCIdNames(*req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("subnet-id", func() []string { + return getAllSubnetIDNames(*req.VPCId, *req.ProjectId, *req.Region) + }) + + cmd.MarkFlagRequired("name") + cmd.MarkFlagRequired("type") + + return cmd +} + +// NewCmdRedisDelete ucloud redis delete +func NewCmdRedisDelete(out io.Writer) *cobra.Command { + var idNames []string + req := base.BizClient.NewDeleteURedisGroupRequest() + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete redis instances", + Long: "Delete redis instances", + Example: "ucloud redis delete --umem-id uredis-rl5xuxx/testcli1,uredis-xsdfa/testcli2", + Run: func(c *cobra.Command, args []string) { + for _, idname := range idNames { + id := base.PickResourceID(idname) + if strings.HasPrefix(id, "uredis") { + req.GroupId = &id + _, err := base.BizClient.DeleteURedisGroup(req) + if err != nil { + base.HandleError(err) + continue + } + } else if strings.HasPrefix(id, "umem") { + _req := base.BizClient.NewDeleteUMemSpaceRequest() + _req.Region = req.Region + _req.Zone = req.Zone + _req.ProjectId = req.ProjectId + _req.SpaceId = &id + _, err := base.BizClient.DeleteUMemSpace(_req) + if err != nil { + base.HandleError(err) + continue + } + } + fmt.Fprintf(out, "redis[%s] deleted\n", idname) + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&idNames, "umem-id", nil, "Required. Resource ID of redis intances to delete") + bindProjectID(req, flags) + bindRegion(req, flags) + bindZone(req, flags) + + cmd.MarkFlagRequired("umem-id") + + flags.SetFlagValuesFunc("umem-id", func() []string { + return getRedisIDList(*req.ProjectId, *req.Region) + }) + + return cmd +} + +// NewCmdRedisRestart ucloud redis restart +func NewCmdRedisRestart(out io.Writer) *cobra.Command { + idNames := make([]string, 0) + req := base.BizClient.UMemClient.NewRestartURedisGroupRequest() + cmd := &cobra.Command{ + Use: "restart", + Short: "Restart redis instances of master-replica type", + Long: "Restart redis instances of master-replica type", + Run: func(c *cobra.Command, args []string) { + reqs := make([]request.Common, len(idNames)) + for idx, idname := range idNames { + id := base.PickResourceID(idname) + _req := *req + _req.GroupId = &id + reqs[idx] = &_req + } + coAction := newConcurrentAction(reqs, 10, restartRedis) + coAction.Do() + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&idNames, "umem-id", nil, "Required. Resource ID of redis instances to restart") + bindProjectID(req, flags) + bindRegion(req, flags) + bindZone(req, flags) + + cmd.MarkFlagRequired("umem-id") + flags.SetFlagValuesFunc("umem-id", func() []string { + return getRedisIDList(*req.ProjectId, *req.Region) + }) + + return cmd +} + +func restartRedis(creq request.Common) (bool, []string) { + req := creq.(*umem.RestartURedisGroupRequest) + block := ux.NewBlock() + ux.Doc.Append(block) + logs := make([]string, 0) + logs = append(logs, fmt.Sprintf("api:RestartURedisGroup, request:%v", base.ToQueryMap(req))) + _, err := base.BizClient.UMemClient.RestartURedisGroup(req) + if err != nil { + block.Append(base.ParseError(err)) + logs = append(logs, fmt.Sprintf("restart redis[%s] failed: %s", *req.GroupId, base.ParseError(err))) + return false, logs + } + poller := base.NewSpoller(describeRedisByID, base.Cxt.GetWriter()) + text := fmt.Sprintf("redis[%s] is restarting", *req.GroupId) + ret := poller.Sspoll(*req.GroupId, text, []string{status.UMEM_RUNNING, status.UMEM_FAIL}, block, nil) + if ret.Err != nil { + block.Append(base.ParseError(err)) + logs = append(logs, ret.Err.Error()) + } + if ret.Timeout { + logs = append(logs, "poll redis[%s] timeout", *req.GroupId) + } + return ret.Done, logs +} + +func getRedisIDList(project, region string) []string { + req := base.BizClient.NewDescribeURedisGroupRequest() + req.ProjectId = &project + req.Region = ®ion + list := []string{} + + for limit, offset := 50, 0; ; offset += limit { + req.Limit = sdk.Int(limit) + req.Offset = sdk.Int(offset) + resp, err := base.BizClient.DescribeURedisGroup(req) + if err != nil { + return nil + } + for _, ins := range resp.DataSet { + list = append(list, fmt.Sprintf("%s/%s", ins.GroupId, ins.Name)) + } + if offset+limit >= resp.TotalCount { + break + } + } + return list +} + +// UMemMemcacheRow 表格行 +type UMemMemcacheRow struct { + ResourceID string + Name string + Address string + Size string + UsedSize string + State string + Group string + CreateTime string +} + +// NewCmdMemcacheList ucloud memcache list +func NewCmdMemcacheList(out io.Writer) *cobra.Command { + req := base.BizClient.NewDescribeUMemcacheGroupRequest() + cmd := &cobra.Command{ + Use: "list", + Short: "List memcache instances", + Long: "List memcache instances", + Run: func(c *cobra.Command, args []string) { + resp, err := base.BizClient.DescribeUMemcacheGroup(req) + if err != nil { + base.HandleError(err) + return + } + list := []UMemMemcacheRow{} + for _, ins := range resp.DataSet { + row := UMemMemcacheRow{ + ResourceID: ins.GroupId, + Name: ins.Name, + Group: ins.Tag, + Size: fmt.Sprintf("%dGB", ins.Size), + UsedSize: fmt.Sprintf("%dMB", ins.UsedSize), + State: ins.State, + CreateTime: base.FormatDate(ins.CreateTime), + Address: fmt.Sprintf("%s:%d", ins.VirtualIP, ins.Port), + } + list = append(list, row) + } + base.PrintList(list, out) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + req.GroupId = flags.String("umem-id", "", "Optional. Resource ID of the redis to list") + bindRegion(req, flags) + bindZoneEmpty(req, flags) + bindProjectID(req, flags) + bindOffset(req, flags) + bindLimit(req, flags) + + return cmd +} + +// NewCmdMemcacheCreate ucloud memcache create +func NewCmdMemcacheCreate(out io.Writer) *cobra.Command { + req := base.BizClient.NewCreateUMemcacheGroupRequest() + cmd := &cobra.Command{ + Use: "create", + Short: "Create memcache instance", + Long: "Create memcache instance", + Run: func(c *cobra.Command, args []string) { + if *req.Size > 32 || *req.Size < 1 { + fmt.Fprintln(out, "size-gb should be between 1 and 32") + return + } + resp, err := base.BizClient.CreateUMemcacheGroup(req) + if err != nil { + base.HandleError(err) + return + } + fmt.Fprintf(out, "memcache[%s] created\n", resp.GroupId) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + req.Name = flags.String("name", "", "Required. Name of memcache instance to create") + req.Size = flags.Int("size-gb", 1, "Optional. Memory size of memcache instance. Unit GB. Accpet values:1,2,4,8,16,32") + req.VPCId = flags.String("vpc-id", "", "Optional. VPC ID. See 'ucloud vpc list'") + req.SubnetId = flags.String("subnet-id", "", "Optional. Subnet ID. See 'ucloud subnet list'") + bindProjectID(req, flags) + bindRegion(req, flags) + bindZone(req, flags) + bindChargeType(req, flags) + bindQuantity(req, flags) + bindGroup(req, flags) + + flags.SetFlagValues("size-gb", "1", "2", "4", "8", "16", "32") + flags.SetFlagValuesFunc("vpc-id", func() []string { + return getAllVPCIdNames(*req.ProjectId, *req.Region) + }) + flags.SetFlagValuesFunc("subnet-id", func() []string { + return getAllSubnetIDNames(*req.VPCId, *req.ProjectId, *req.Region) + }) + + cmd.MarkFlagRequired("name") + + return cmd +} + +// NewCmdMemcacheDelete ucloud memcache delete +func NewCmdMemcacheDelete(out io.Writer) *cobra.Command { + var idNames []string + req := base.BizClient.NewDeleteUMemcacheGroupRequest() + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete memcache instances", + Long: "Delete memcache instances", + Example: "ucloud memcache delete --umem-id umemcache-rl5xuxx/testcli1,umemcache-xsdfa/testcli2", + Run: func(c *cobra.Command, args []string) { + for _, idname := range idNames { + id := base.PickResourceID(idname) + req.GroupId = &id + _, err := base.BizClient.DeleteUMemcacheGroup(req) + if err != nil { + base.HandleError(err) + continue + } + fmt.Fprintf(out, "memcache[%s] deleted\n", idname) + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&idNames, "umem-id", nil, "Required. Resource ID of memcache intances to delete") + bindProjectID(req, flags) + bindRegion(req, flags) + bindZoneEmpty(req, flags) + + cmd.MarkFlagRequired("umem-id") + + flags.SetFlagValuesFunc("umem-id", func() []string { + return getMemcacheIDList(*req.ProjectId, *req.Region) + }) + + return cmd +} + +// NewCmdMemcacheRestart ucloud memcache restart +func NewCmdMemcacheRestart(out io.Writer) *cobra.Command { + idNames := make([]string, 0) + req := base.BizClient.NewRestartUMemcacheGroupRequest() + cmd := &cobra.Command{ + Use: "restart", + Short: "Restart memcache instances", + Long: "Restart memcache instances", + Run: func(c *cobra.Command, args []string) { + reqs := make([]request.Common, len(idNames)) + for idx, idname := range idNames { + id := base.PickResourceID(idname) + _req := *req + _req.GroupId = &id + reqs[idx] = &_req + } + coAction := newConcurrentAction(reqs, 10, restartMemcache) + coAction.Do() + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&idNames, "umem-id", nil, "Required. Resource ID of memcache to restart") + bindRegion(req, flags) + bindZone(req, flags) + bindProjectID(req, flags) + + flags.SetFlagValuesFunc("umem-id", func() []string { + return getMemcacheIDList(*req.ProjectId, *req.Region) + }) + + cmd.MarkFlagRequired("umem-id") + return cmd +} + +func restartMemcache(creq request.Common) (bool, []string) { + req := creq.(*umem.RestartUMemcacheGroupRequest) + block := ux.NewBlock() + ux.Doc.Append(block) + logs := make([]string, 0) + logs = append(logs, fmt.Sprintf("api:RestartUMemcacheGroup, request:%v", base.ToQueryMap(req))) + _, err := base.BizClient.RestartUMemcacheGroup(req) + if err != nil { + block.Append(base.ParseError(err)) + logs = append(logs, fmt.Sprintf("restart memcache[%s] failed: %s", *req.GroupId, base.ParseError(err))) + return false, logs + } + poller := base.NewSpoller(describeMemcacheByID, base.Cxt.GetWriter()) + text := fmt.Sprintf("memcache[%s] is restarting", *req.GroupId) + ret := poller.Sspoll(*req.GroupId, text, []string{status.UMEM_RUNNING, status.UMEM_FAIL}, block, nil) + if ret.Err != nil { + block.Append(base.ParseError(err)) + logs = append(logs, ret.Err.Error()) + } + if ret.Timeout { + logs = append(logs, "poll memcache[%s] timeout", *req.GroupId) + } + return ret.Done, logs +} + +func describeMemcacheByID(memcacheID string, commonBase *request.CommonBase) (interface{}, error) { + req := base.BizClient.NewDescribeUMemRequest() + if commonBase != nil { + req.CommonBase = *commonBase + } + req.Protocol = sdk.String("memcache") + req.ResourceId = &memcacheID + + resp, err := base.BizClient.DescribeUMem(req) + if err != nil { + return nil, err + } + if len(resp.DataSet) < 1 { + return nil, fmt.Errorf(fmt.Sprintf("resource [%s] may not exist", memcacheID)) + } + return &resp.DataSet[0], nil +} +func describeRedisByID(redisID string, commonBase *request.CommonBase) (interface{}, error) { + req := base.BizClient.NewDescribeUMemRequest() + if commonBase != nil { + req.CommonBase = *commonBase + } + req.Protocol = sdk.String("redis") + req.ResourceId = &redisID + + resp, err := base.BizClient.DescribeUMem(req) + if err != nil { + return nil, err + } + if len(resp.DataSet) < 1 { + return nil, fmt.Errorf(fmt.Sprintf("resource [%s] may not exist", redisID)) + } + return &resp.DataSet[0], nil +} + +func getMemcacheIDList(project, region string) []string { + req := base.BizClient.NewDescribeUMemcacheGroupRequest() + req.ProjectId = &project + req.Region = ®ion + list := []string{} + + for limit, offset := 50, 0; ; offset += limit { + req.Limit = sdk.Int(limit) + req.Offset = sdk.Int(offset) + resp, err := base.BizClient.DescribeUMemcacheGroup(req) + if err != nil { + fmt.Println(err) + return nil + } + for _, ins := range resp.DataSet { + list = append(list, fmt.Sprintf("%s/%s", ins.GroupId, ins.Name)) + } + if offset+limit >= resp.TotalCount { + break + } + } + return list +} diff --git a/cmd/unet.go b/cmd/unet.go index 789af065d3..f4dead67c2 100644 --- a/cmd/unet.go +++ b/cmd/unet.go @@ -16,198 +16,245 @@ package cmd import ( "fmt" - "strings" + "io" "github.com/spf13/cobra" - . "github.com/ucloud/ucloud-cli/base" + + "github.com/ucloud/ucloud-sdk-go/services/udpn" + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + + "github.com/ucloud/ucloud-cli/base" ) -//NewCmdSubnet ucloud subnet -func NewCmdSubnet() *cobra.Command { +// NewCmdUDPN ucloud udpn +func NewCmdUDPN(out io.Writer) *cobra.Command { cmd := &cobra.Command{ - Use: "subnet", - Short: "List subnet", - Long: `List subnet`, - Args: cobra.NoArgs, + Use: "udpn", + Short: "List and manipulate udpn instances", + Long: "List and manipulate udpn instances", } - cmd.AddCommand(NewCmdSubnetList()) - return cmd -} + cmd.AddCommand(NewCmdUDPNCreate(out)) + cmd.AddCommand(NewCmdUDPNList(out)) + cmd.AddCommand(NewCmdUdpnDelete(out)) + cmd.AddCommand(NewCmdUdpnModifyBW(out)) -//SubnetRow 表格行 -type SubnetRow struct { - SubnetName string - ResourceID string - Group string - AffiliatedVPC string - NetworkSegment string - CreationTime string + return cmd } -//NewCmdSubnetList ucloud subnet list -func NewCmdSubnetList() *cobra.Command { - req := BizClient.NewDescribeSubnetRequest() +// NewCmdUDPNCreate ucloud udpn create +func NewCmdUDPNCreate(out io.Writer) *cobra.Command { + req := base.BizClient.NewAllocateUDPNRequest() cmd := &cobra.Command{ - Use: "list", - Short: "List subnet", - Long: `List subnet`, - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - resp, err := BizClient.DescribeSubnet(req) - if err != nil { - HandleError(err) + Use: "create", + Short: "Create UDPN tunnel", + Long: "Create UDPN tunnel", + Run: func(c *cobra.Command, args []string) { + if *req.Peer1 == *req.Peer2 { + fmt.Fprintln(out, "Error, flags peer1 and peer2 can't be equal") return } - if global.json { - PrintJSON(resp.DataSet) - } else { - list := make([]SubnetRow, 0) - for _, sn := range resp.DataSet { - row := SubnetRow{} - row.SubnetName = sn.SubnetName - row.ResourceID = sn.SubnetId - row.Group = sn.Tag - row.AffiliatedVPC = fmt.Sprintf("%s/%s", sn.VPCName, sn.VPCId) - row.NetworkSegment = fmt.Sprintf("%s/%s", sn.Subnet, sn.Netmask) - row.CreationTime = FormatDate(sn.CreateTime) - list = append(list, row) - } - PrintTable(list, []string{"SubnetName", "ResourceID", "Group", "AffiliatedVPC", "NetworkSegment", "CreationTime"}) + resp, err := base.BizClient.AllocateUDPN(req) + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + if err != nil { + base.HandleError(err) + return } + fmt.Fprintf(out, "udpn[%s] created\n", resp.UDPNId) }, } flags := cmd.Flags() flags.SortFlags = false - req.Region = flags.String("region", ConfigInstance.Region, "Optional. Region, see 'ucloud region'") - req.ProjectId = flags.String("project-id", ConfigInstance.ProjectID, "Optional. Project-id, see 'ucloud project list'") - flags.StringSliceVar(&req.SubnetIds, "subnet-id", []string{}, "Optional. Multiple values separated by commas") - req.VPCId = flags.String("vpc-id", "", "Optional. ResourceID of VPC") - req.Tag = flags.String("group", "", "Optional. Group") - req.Offset = flags.Int("offset", 0, "Optional. offset default 0") - req.Limit = flags.Int("limit", 50, "Optional. max count") + + req.Peer1 = flags.String("peer1", base.ConfigIns.Region, "Required. One end of the tunnel to create") + req.Peer2 = flags.String("peer2", "", "Required. The other end of the tunnel create") + req.Bandwidth = flags.Int("bandwidth-mb", 0, "Required. Bandwidth of the tunnel to create. Unit:Mb. Rnange [2,1000]") + req.ChargeType = flags.String("charge-type", "", "Optional. Enumeration value.'Year',pay yearly;'Month',pay monthly;'Dynamic', pay hourly") + req.Quantity = cmd.Flags().Int("quantity", 1, "Optional. The duration of the instance. N years/months.") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Project-id, see 'ucloud project list'") + + flags.SetFlagValues("charge-type", "Month", "Year", "Dynamic") + flags.SetFlagValuesFunc("project-id", getProjectList) + flags.SetFlagValuesFunc("peer1", getRegionList) + //peer1和peer2不相等 + flags.SetFlagValuesFunc("peer2", func() []string { + regions := getRegionList() + list := []string{} + for _, r := range regions { + if r != *req.Peer1 { + list = append(list, r) + } + } + return list + }) + + cmd.MarkFlagRequired("peer1") + cmd.MarkFlagRequired("peer2") + cmd.MarkFlagRequired("bandwidth-mb") return cmd } -//VPCRow 表格行 -type VPCRow struct { - VPCName string - ResourceID string - Group string - NetworkSegment string - SubnetCount int - CreationTime string +// UDPNRow 表格行 +type UDPNRow struct { + ResourceID string + Peers string + Bandwidth string + ChargeType string + CreationTime string } -//NewCmdVPCList ucloud vpc list -func NewCmdVPCList() *cobra.Command { - req := BizClient.NewDescribeVPCRequest() +// NewCmdUDPNList ucloud udpn list +func NewCmdUDPNList(out io.Writer) *cobra.Command { + req := base.BizClient.NewDescribeUDPNRequest() cmd := &cobra.Command{ Use: "list", - Short: "List vpc", - Long: "List vpc", - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - resp, err := BizClient.DescribeVPC(req) + Short: "List udpn instances", + Long: "List udpn instances", + Run: func(c *cobra.Command, args []string) { + req.UDPNId = sdk.String(base.PickResourceID(*req.UDPNId)) + resp, err := base.BizClient.DescribeUDPN(req) if err != nil { - HandleError(err) + base.HandleError(err) return } - if global.json { - PrintJSON(resp.DataSet) - } else { - list := []VPCRow{} - for _, vpc := range resp.DataSet { - row := VPCRow{} - row.VPCName = vpc.Name - row.ResourceID = vpc.VPCId - row.Group = vpc.Tag - row.NetworkSegment = strings.Join(vpc.Network, ",") - row.SubnetCount = vpc.SubnetCount - row.CreationTime = FormatDate(vpc.CreateTime) - list = append(list, row) - } - PrintTable(list, []string{"VPCName", "ResourceID", "Group", "NetworkSegment", "SubnetCount", "CreationTime"}) + list := []UDPNRow{} + for _, udpn := range resp.DataSet { + row := UDPNRow{} + row.ResourceID = udpn.UDPNId + row.Peers = fmt.Sprintf("%s <--> %s", udpn.Peer1, udpn.Peer2) + row.Bandwidth = fmt.Sprintf("%dMb", udpn.Bandwidth) + row.ChargeType = udpn.ChargeType + row.CreationTime = base.FormatDate(udpn.CreateTime) + list = append(list, row) } - + base.PrintList(list, out) }, } + flags := cmd.Flags() flags.SortFlags = false - req.Region = flags.String("region", ConfigInstance.Region, "Optional. Region, see 'ucloud region'") - req.ProjectId = flags.String("project-id", ConfigInstance.ProjectID, "Optional. Project-id, see 'ucloud project list'") - req.Tag = flags.String("group", "", "Optional. Group") - flags.StringSliceVar(&req.VPCIds, "vpc-id", []string{}, "Optional. Multiple values separated by commas") + + req.UDPNId = flags.String("udpn-id", "", "Optional. Resource ID of udpn instances to list") + req.Offset = flags.Int("offset", 0, "Optional. Offset") + req.Limit = flags.Int("limit", 50, "Optional. Limit") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Region, see 'ucloud region'") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Project-id, see 'ucloud project list'") + + flags.SetFlagValuesFunc("region", getRegionList) + flags.SetFlagValuesFunc("project-id", getRegionList) + flags.SetFlagValuesFunc("udpn-id", func() []string { + return getAllUDPNIdNames(*req.ProjectId, *req.Region) + }) return cmd } -//NewCmdFirewall ucloud firewall -func NewCmdFirewall() *cobra.Command { +// NewCmdUdpnDelete ucloud udpn delete +func NewCmdUdpnDelete(out io.Writer) *cobra.Command { + idNames := []string{} + req := base.BizClient.NewReleaseUDPNRequest() cmd := &cobra.Command{ - Use: "firewall", - Short: "List extranet firewall", - Long: `List extranet firewall`, - Args: cobra.NoArgs, + Use: "delete", + Short: "delete udpn instances", + Long: "delete udpn instances", + Run: func(c *cobra.Command, args []string) { + for _, idname := range idNames { + req.UDPNId = sdk.String(base.PickResourceID(idname)) + _, err := base.BizClient.ReleaseUDPN(req) + if err != nil { + base.HandleError(err) + continue + } + fmt.Fprintf(out, "udpn[%s] deleted\n", idname) + } + }, } - cmd.AddCommand(NewCmdFirewallList()) + flags := cmd.Flags() + flags.SortFlags = false - return cmd -} + flags.StringSliceVar(&idNames, "udpn-id", nil, "Required. Resource ID of udpn instances to delete") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Project-id, see 'ucloud project list'") + + flags.SetFlagValuesFunc("project-id", getRegionList) + flags.SetFlagValuesFunc("udpn-id", func() []string { + return getAllUDPNIdNames(*req.ProjectId, base.ConfigIns.Region) + }) + + cmd.MarkFlagRequired("udpn-id") -//FirewallRow 表格行 -type FirewallRow struct { - ResourceID string - FirewallName string - Remark string - Group string - RuleAmount int - BoundResourceAmount int - CreationTime string + return cmd } -//NewCmdFirewallList ucloud firewall list -func NewCmdFirewallList() *cobra.Command { - req := BizClient.NewDescribeFirewallRequest() +// NewCmdUdpnModifyBW ucloud udpn modify-bw +func NewCmdUdpnModifyBW(out io.Writer) *cobra.Command { + idNames := []string{} + req := base.BizClient.NewModifyUDPNBandwidthRequest() cmd := &cobra.Command{ - Use: "list", - Short: "List extranet firewall", - Long: `List extranet firewall`, - Run: func(cmd *cobra.Command, args []string) { - resp, err := BizClient.DescribeFirewall(req) - if err != nil { - HandleError(err) - return - } - if global.json { - PrintJSON(resp.DataSet) - } else { - list := []FirewallRow{} - for _, fw := range resp.DataSet { - row := FirewallRow{} - row.ResourceID = fw.FWId - row.FirewallName = fw.Name - row.Remark = fw.Remark - row.Group = fw.Tag - row.RuleAmount = len(fw.Rule) - row.BoundResourceAmount = fw.ResourceCount - row.CreationTime = FormatDate(fw.CreateTime) - list = append(list, row) + Use: "modify-bw", + Short: "Modify bandwidth of UDPN tunnel", + Long: "Modify bandwidth of UDPN tunnel", + Run: func(c *cobra.Command, args []string) { + for _, idname := range idNames { + req.UDPNId = sdk.String(base.PickResourceID(idname)) + _, err := base.BizClient.ModifyUDPNBandwidth(req) + if err != nil { + base.HandleError(err) + return } - PrintTable(list, []string{"ResourceID", "FirewallName", "Remark", "Group", "RuleAmount", "BoundResourceAmount", "CreationTime"}) + fmt.Fprintf(out, "udpn[%s]'s bandwidth modified\n", idname) } }, } flags := cmd.Flags() flags.SortFlags = false - req.Region = flags.String("region", ConfigInstance.Region, "Optional. Region, see 'ucloud region'") - req.ProjectId = flags.String("project-id", ConfigInstance.ProjectID, "Optional. Project-id, see 'ucloud project list'") - req.FWId = flags.String("firewall-id", "", "Optional. The Resource ID of firewall. Return all firewalls by default.") - req.ResourceType = flags.String("bound-resource-type", "", "Optional. The type of resource bound on the firewall") - req.ResourceId = flags.String("bound-resource-id", "", "Optional. The resource ID of resource bound on the firewall") - req.Offset = flags.String("offset", "0", "Optional. offset default 0") - req.Limit = flags.String("limit", "50", "Optional. max count") + + flags.StringSliceVar(&idNames, "udpn-id", nil, "Required. Resource ID of UDPN to modify bandwidth") + req.Bandwidth = flags.Int("bandwidth-mb", 0, "Required. Bandwidth of UDPN tunnel. Unit:Mb. Range [2,1000]") + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Region, see 'ucloud region'") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Project-id, see 'ucloud project list'") + + flags.SetFlagValuesFunc("udpn-id", func() []string { + return getAllUDPNIdNames(*req.ProjectId, *req.Region) + }) + + cmd.MarkFlagRequired("udpn-id") + cmd.MarkFlagRequired("bandwidth-mb") + return cmd } + +func getAllUDPNIns(project, region string) ([]udpn.UDPNData, error) { + req := base.BizClient.NewDescribeUDPNRequest() + req.ProjectId = sdk.String(project) + req.Region = sdk.String(region) + list := make([]udpn.UDPNData, 0) + for offset, limit := 0, 50; ; offset += limit { + req.Offset = sdk.Int(offset) + req.Limit = sdk.Int(limit) + resp, err := base.BizClient.DescribeUDPN(req) + if err != nil { + return nil, err + } + for _, u := range resp.DataSet { + list = append(list, u) + } + if offset+limit > resp.TotalCount { + break + } + } + return list, nil +} + +func getAllUDPNIdNames(project, region string) []string { + udpnInsList, err := getAllUDPNIns(project, region) + if err != nil { + return nil + } + idNameList := []string{} + for _, udpn := range udpnInsList { + idNameList = append(idNameList, fmt.Sprintf("%s/%s:%s", udpn.UDPNId, udpn.Peer1, udpn.Peer2)) + } + return idNameList +} diff --git a/cmd/uphost.go b/cmd/uphost.go new file mode 100644 index 0000000000..e902c0130b --- /dev/null +++ b/cmd/uphost.go @@ -0,0 +1,102 @@ +// Copyright © 2018 NAME HERE tony.li@ucloud.cn +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + "github.com/ucloud/ucloud-cli/base" +) + +// NewCmdUPHost ucloud uphost +func NewCmdUPHost() *cobra.Command { + cmd := &cobra.Command{ + Use: "uphost", + Short: "List UPHost instances", + Long: `List UPHost instances`, + Args: cobra.NoArgs, + } + out := base.Cxt.GetWriter() + cmd.AddCommand(NewCmdUPHostList(out)) + + return cmd +} + +type uphostRow struct { + ResourceID string + Name string + PrivateIP string + PublicIP string + Config string + Image string + HostType string + Status string + Group string +} + +// NewCmdUPHostList ucloud uphost list +func NewCmdUPHostList(out io.Writer) *cobra.Command { + ids := []string{} + req := base.BizClient.NewDescribePHostRequest() + cmd := &cobra.Command{ + Use: "list", + Short: "List UPHost instances", + Long: "List UPHost instances", + Run: func(c *cobra.Command, args []string) { + resp, err := base.BizClient.DescribePHost(req) + if err != nil { + base.HandleError(err) + return + } + list := make([]uphostRow, 0) + for _, ins := range resp.PHostSet { + row := uphostRow{ + ResourceID: ins.PHostId, + Name: ins.Name, + Config: fmt.Sprintf("core:%d memory:%dG", ins.CPUSet.CoreCount, ins.Memory/1024), + Group: ins.Tag, + HostType: ins.PHostType, + Status: ins.PMStatus, + Image: ins.ImageName, + } + for _, ip := range ins.IPSet { + if ip.OperatorName == "Private" { + row.PrivateIP = ip.IPAddr + } else { + row.PublicIP = ip.IPAddr + " " + ip.OperatorName + } + } + for _, disk := range ins.DiskSet { + if disk.Name == "data" { + row.Config += fmt.Sprintf(" data-disk:%dG %s", disk.Space, disk.Type) + } + } + list = append(list, row) + } + base.PrintList(list, out) + }, + } + flags := cmd.Flags() + bindRegion(req, flags) + bindZoneEmpty(req, flags) + bindProjectID(req, flags) + bindOffset(req, flags) + bindLimit(req, flags) + flags.StringSliceVar(&ids, "uphost-id", nil, "Optional. Resource ID of uphost instances. List those specified uphost instances") + + return cmd +} diff --git a/cmd/util.go b/cmd/util.go new file mode 100644 index 0000000000..3a0347bdfa --- /dev/null +++ b/cmd/util.go @@ -0,0 +1,191 @@ +package cmd + +import ( + "fmt" + "reflect" + "strings" + "sync" + "time" + + "github.com/spf13/pflag" + + "github.com/ucloud/ucloud-sdk-go/ucloud/request" + + "github.com/ucloud/ucloud-cli/base" + "github.com/ucloud/ucloud-cli/ux" +) + +func bindRegion(req request.Common, flags *pflag.FlagSet) { + var region string + flags.StringVar(®ion, "region", base.ConfigIns.Region, "Optional. Override default region for this command invocation, see 'ucloud region'") + flags.SetFlagValuesFunc("region", getRegionList) + req.SetRegionRef(®ion) +} + +func bindRegionS(region *string, flags *pflag.FlagSet) { + *region = base.ConfigIns.Region + flags.StringVar(region, "region", base.ConfigIns.Region, "Optional. Override default region for this command invocation, see 'ucloud region'") + flags.SetFlagValuesFunc("region", getRegionList) +} + +func bindZone(req request.Common, flags *pflag.FlagSet) { + var zone string + flags.StringVar(&zone, "zone", base.ConfigIns.Zone, "Optional. Override default availability zone for this command invocation, see 'ucloud region'") + flags.SetFlagValuesFunc("zone", func() []string { + return getZoneList(req.GetRegion()) + }) + req.SetZoneRef(&zone) +} + +func bindZoneEmpty(req request.Common, flags *pflag.FlagSet) { + var zone string + flags.StringVar(&zone, "zone", "", "Optional. Override default availability zone for this command invocation, see 'ucloud region'") + flags.SetFlagValuesFunc("zone", func() []string { + return getZoneList(req.GetRegion()) + }) + req.SetZoneRef(&zone) +} + +func bindZoneEmptyS(zone, region *string, flags *pflag.FlagSet) { + flags.StringVar(zone, "zone", "", "Optional. Override default availability zone for this command invocation, see 'ucloud region'") + flags.SetFlagValuesFunc("zone", func() []string { + return getZoneList(*region) + }) +} + +func bindZoneS(zone, region *string, flags *pflag.FlagSet) { + *zone = base.ConfigIns.Zone + flags.StringVar(zone, "zone", base.ConfigIns.Zone, "Optional. Override default availability zone for this command invocation, see 'ucloud region'") + flags.SetFlagValuesFunc("zone", func() []string { + return getZoneList(*region) + }) +} + +func bindProjectID(req request.Common, flags *pflag.FlagSet) { + var project string + flags.StringVar(&project, "project-id", base.ConfigIns.ProjectID, "Optional. Override default project-id for this command invocation, see 'ucloud project list'") + flags.SetFlagValuesFunc("project-id", getProjectList) + req.SetProjectIdRef(&project) +} + +func bindProjectIDS(project *string, flags *pflag.FlagSet) { + *project = base.ConfigIns.ProjectID + flags.StringVar(project, "project-id", base.ConfigIns.ProjectID, "Optional. Override default project-id for this command invocation, see 'ucloud project list'") + flags.SetFlagValuesFunc("project-id", getProjectList) +} + +func bindGroup(req interface{}, flags *pflag.FlagSet) { + group := flags.String("group", "", "Optional. Business group") + v := reflect.ValueOf(req).Elem() + f := v.FieldByName("Tag") + f.Set(reflect.ValueOf(group)) +} + +func bindLimit(req interface{}, flags *pflag.FlagSet) { + limit := flags.Int("limit", 100, "Optional. The maximum number of resources per page") + v := reflect.ValueOf(req).Elem() + f := v.FieldByName("Limit") + f.Set(reflect.ValueOf(limit)) +} + +func bindOffset(req interface{}, flags *pflag.FlagSet) { + offset := flags.Int("offset", 0, "Optional. The index(a number) of resource which start to list") + v := reflect.ValueOf(req).Elem() + f := v.FieldByName("Offset") + f.Set(reflect.ValueOf(offset)) +} + +func bindChargeType(req interface{}, flags *pflag.FlagSet) { + chargeType := flags.String("charge-type", "Month", "Optional. Enumeration value.'Year',pay yearly;'Month',pay monthly; 'Dynamic', pay hourly; 'Trial', free trial(need permission)") + v := reflect.ValueOf(req).Elem() + f := v.FieldByName("ChargeType") + f.Set(reflect.ValueOf(chargeType)) + flags.SetFlagValues("charge-type", "Month", "Dynamic", "Year") +} + +func bindQuantity(req interface{}, flags *pflag.FlagSet) { + quanitiy := flags.Int("quantity", 1, "Optional. The duration of the instance. N years/months.") + v := reflect.ValueOf(req).Elem() + f := v.FieldByName("Quantity") + f.Set(reflect.ValueOf(quanitiy)) +} + +func getEIPLine(region string) (line string) { + if strings.HasPrefix(region, "cn") { + line = "BGP" + } else { + line = "International" + } + return +} + +type concurrentAction struct { + reqs []request.Common + actionFunc func(request.Common) (bool, []string) + wg *sync.WaitGroup + result chan bool + tokens chan bool +} + +func newConcurrentAction(reqs []request.Common, limit int, actionFunc func(request.Common) (bool, []string)) *concurrentAction { + if limit <= 0 { + limit = 10 + } + return &concurrentAction{ + reqs: reqs, + actionFunc: actionFunc, + wg: &sync.WaitGroup{}, + result: make(chan bool), + tokens: make(chan bool, limit), //控制并发量,最多是个并发 + } +} + +func (c *concurrentAction) actionFuncWrapper(req request.Common) { + c.tokens <- true + success, logs := c.actionFunc(req) + c.result <- success + logs = append([]string{"========================================"}, logs...) + base.LogInfo(logs...) + <-c.tokens + time.Sleep(time.Second / 5) + c.wg.Done() +} + +func (c *concurrentAction) Do() { + count := len(c.reqs) + success, fail := 0, 0 + refresh := ux.NewRefresh() + //同时执行任务数量大于5时,不再单独显示每一个任务的进行情况,而是聚合显示 + if count > 5 { + ux.Doc.Disable() + refresh.Do(fmt.Sprintf("total:%d, doing:%d, success:%d, fail:%d", count, len(c.tokens), success, fail)) + } + go func() { + for { + select { + case ret := <-c.result: + if ret { + success++ + } else { + fail++ + } + + case <-time.Tick(time.Second / 30): + if count == (success+fail) && fail > 0 { + fmt.Printf("Check logs in %s\n", base.GetLogFilePath()) + return + } + if count > 5 { + refresh.Do(fmt.Sprintf("total:%d, doing:%d, success:%d, fail:%d", count, len(c.tokens), success, fail)) + } + } + } + }() + + for _, req := range c.reqs { + c.wg.Add(1) + go c.actionFuncWrapper(req) + } + + c.wg.Wait() +} diff --git a/cmd/vpc.go b/cmd/vpc.go index f5f98c9920..f6ecd02db9 100644 --- a/cmd/vpc.go +++ b/cmd/vpc.go @@ -1,30 +1,40 @@ package cmd import ( + "fmt" + "io" + "net" + "strconv" + "strings" + "github.com/spf13/cobra" - . "github.com/ucloud/ucloud-cli/base" + + "github.com/ucloud/ucloud-sdk-go/services/vpc" + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + + "github.com/ucloud/ucloud-cli/base" ) -//NewCmdVpc ucloud vpc +// NewCmdVpc ucloud vpc func NewCmdVpc() *cobra.Command { cmd := &cobra.Command{ Use: "vpc", - Short: "List vpc", - Long: "List vpc", + Short: "List and manipulate VPC instances", + Long: "List and manipulate VPC instances", Args: cobra.NoArgs, } - + out := base.Cxt.GetWriter() cmd.AddCommand(NewCmdVpcCreate()) - cmd.AddCommand(NewCmdVPCList()) + cmd.AddCommand(NewCmdVPCList(out)) cmd.AddCommand(NewCmdVpcDelete()) cmd.AddCommand(NewCmdVpcCreatePeer()) - cmd.AddCommand(NewCmdVpcListPeer()) + cmd.AddCommand(NewCmdVpcListPeer(out)) cmd.AddCommand(NewCmdVpcDeletePeer()) - cmd.AddCommand(NewCmdSubnetCreate()) return cmd } -/* type VPCRow struct { +// VPCRow 表格行 +type VPCRow struct { VPCName string ResourceID string Group string @@ -33,43 +43,70 @@ func NewCmdVpc() *cobra.Command { CreationTime string } -type SubnetRow struct { - SubnetName string - ResourceID string - Group string - AffiliatedVPC string - NetworkSegment string - CreationTime string -} -*/ +// NewCmdVPCList ucloud vpc list +func NewCmdVPCList(out io.Writer) *cobra.Command { + vpcIDs := []string{} + req := base.BizClient.NewDescribeVPCRequest() + cmd := &cobra.Command{ + Use: "list", + Short: "List vpc", + Long: "List vpc", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + for _, id := range vpcIDs { + req.VPCIds = append(req.VPCIds, base.PickResourceID(id)) + } + resp, err := base.BizClient.DescribeVPC(req) + if err != nil { + base.HandleError(err) + return + } + list := []VPCRow{} + for _, vpc := range resp.DataSet { + row := VPCRow{} + row.VPCName = vpc.Name + row.ResourceID = vpc.VPCId + row.Group = vpc.Tag + row.NetworkSegment = strings.Join(vpc.Network, ",") + row.SubnetCount = vpc.SubnetCount + row.CreationTime = base.FormatDate(vpc.CreateTime) + list = append(list, row) + } + base.PrintList(list, out) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Region, see 'ucloud region'") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Project-id, see 'ucloud project list'") + req.Tag = flags.String("group", "", "Optional. Group") + flags.StringSliceVar(&vpcIDs, "vpc-id", []string{}, "Optional. Multiple values separated by commas") -type VPCIntercomRow struct { - ProjectId string - Network []string - DstRegion string - Name string - VPCId string - Tag string + flags.SetFlagValuesFunc("vpc-id", func() []string { + return getAllVPCIdNames(*req.ProjectId, *req.Region) + }) + + return cmd } -//NewCreateVPCRequest ucloud vpc create +// NewCmdVpcCreate ucloud vpc create func NewCmdVpcCreate() *cobra.Command { var segments *[]string - req := BizClient.NewCreateVPCRequest() + req := base.BizClient.NewCreateVPCRequest() cmd := &cobra.Command{ Use: "create", Short: "Create vpc network", Long: "Create vpc network", - Example: "ucloud vpc create --name xxx --segment xxx", + Example: "ucloud vpc create --name xxx --segment 192.168.0.0/16", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { req.Network = *segments - resp, err := BizClient.CreateVPC(req) + resp, err := base.BizClient.CreateVPC(req) if err != nil { - HandleError(err) + base.HandleError(err) return } - Cxt.Printf("VPC: %v created successfully!\n", resp.VPCId) + base.Cxt.Printf("vpc[%s] created\n", resp.VPCId) }, } flags := cmd.Flags() @@ -77,174 +114,460 @@ func NewCmdVpcCreate() *cobra.Command { req.Name = cmd.Flags().String("name", "", "Required. Name of the vpc network.") segments = cmd.Flags().StringSlice("segment", nil, "Required. The segment for private network.") - req.Tag = cmd.Flags().String("Group", "Default", "Optional. Business group.") - req.Remark = cmd.Flags().String("Remark", "Default", "Optional. The description of the vpc.") - req.Region = cmd.Flags().String("region", ConfigInstance.Region, "Optional. Assign the region of the VPC") - req.ProjectId = cmd.Flags().String("project-id", ConfigInstance.ProjectID, "Optional. Assign the project-id") + req.Tag = cmd.Flags().String("group", "", "Optional. Business group.") + req.Remark = cmd.Flags().String("remark", "", "Optional. The description of the vpc.") + req.Region = cmd.Flags().String("region", base.ConfigIns.Region, "Optional. Assign the region of the VPC") + req.ProjectId = cmd.Flags().String("project-id", base.ConfigIns.ProjectID, "Optional. Assign the project-id") + + flags.SetFlagValuesFunc("vpc-id", func() []string { + return getAllVPCIdNames(*req.ProjectId, *req.Region) + }) + cmd.MarkFlagRequired("name") cmd.MarkFlagRequired("segment") return cmd } -//NewDeleteVPCRequest ucloud vpc delete +// NewCmdVpcDelete ucloud vpc delete func NewCmdVpcDelete() *cobra.Command { - req := BizClient.NewDeleteVPCRequest() + idNames := []string{} + req := base.BizClient.NewDeleteVPCRequest() cmd := &cobra.Command{ Use: "delete", Short: "Delete vpc network", Long: "Delete vpc network", - Example: "ucloud vpc delete --vpc-id", + Example: "ucloud vpc delete --vpc-id uvnet-xxx", Run: func(cmd *cobra.Command, args []string) { - resp, err := BizClient.DeleteVPC(req) - if err != nil { - HandleError(err) - } else { - Cxt.Printf("VPC [%s] was successfully deleted\n ", resp) + for _, idname := range idNames { + req.VPCId = sdk.String(base.PickResourceID(idname)) + _, err := base.BizClient.DeleteVPC(req) + if err != nil { + base.HandleError(err) + return + } + base.Cxt.Printf("vpc[%s] deleted\n", idname) } }, } cmd.Flags().SortFlags = false - req.VPCId = cmd.Flags().String("vpc-id", "", "Required. The vpc network you want to delete") - req.Region = cmd.Flags().String("region", ConfigInstance.Region, "Optional. Clarify the region of the vpc") - req.ProjectId = cmd.Flags().String("project-id", ConfigInstance.ProjectID, "Optional. The project id of the vpc") + cmd.Flags().StringSliceVar(&idNames, "vpc-id", nil, "Required. Resource ID of the vpc network to delete") + req.Region = cmd.Flags().String("region", base.ConfigIns.Region, "Optional. Region of the vpc") + req.ProjectId = cmd.Flags().String("project-id", base.ConfigIns.ProjectID, "Optional. Project id of the vpc") + + cmd.Flags().SetFlagValuesFunc("vpc-id", func() []string { + return getAllVPCIdNames(*req.ProjectId, *req.Region) + }) + cmd.MarkFlagRequired("vpc-id") return cmd } -//NewCreateVPCIntercomRequest ucloud vpc peer +// NewCmdVpcCreatePeer ucloud vpc peer func NewCmdVpcCreatePeer() *cobra.Command { - req := BizClient.NewCreateVPCIntercomRequest() + req := base.BizClient.NewCreateVPCIntercomRequest() cmd := &cobra.Command{ Use: "create-intercome", - Short: "create intercome with other vpc", - Long: "create intercome with other vpc", - Example: "ucloud vpc create-intercome --vpc-id --dstvpc-id --destregion", + Short: "Create intercome with other vpc", + Long: "Create intercome with other vpc", + Example: "ucloud vpc create-intercome --vpc-id xx --dst-vpc-id xx --dst-region xx", Run: func(cmd *cobra.Command, args []string) { - resp, err := BizClient.CreateVPCIntercom(req) + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + req.DstProjectId = sdk.String(base.PickResourceID(*req.DstProjectId)) + req.VPCId = sdk.String(base.PickResourceID(*req.VPCId)) + req.DstVPCId = sdk.String(base.PickResourceID(*req.DstVPCId)) + _, err := base.BizClient.CreateVPCIntercom(req) if err != nil { - HandleError(err) + base.HandleError(err) return } - Cxt.Printf("The intercome [%s] has been establish", resp) + base.Cxt.Printf("intercome [%s<-->%s] establish", *req.VPCId, *req.DstVPCId) }, } cmd.Flags().SortFlags = false req.VPCId = cmd.Flags().String("vpc-id", "", "Required. The source vpc you want to establish the intercome") - req.DstVPCId = cmd.Flags().String("dstvpc-id", "", "Required. The target vpc you want to establish the intercome") - req.DstRegion = cmd.Flags().String("dstregion", "", "Required. If the intercome established across different regions") - req.Region = cmd.Flags().String("region", ConfigInstance.Region, "Optioanl. The region of source vpc which will establish the intercome") - req.ProjectId = cmd.Flags().String("project-id", ConfigInstance.ProjectID, "Optional. The project id of the source vpc") + req.DstVPCId = cmd.Flags().String("dst-vpc-id", "", "Required. The target vpc you want to establish the intercome") + req.DstRegion = cmd.Flags().String("dst-region", base.ConfigIns.Region, "Required. If the intercome established across different regions") + req.Region = cmd.Flags().String("region", base.ConfigIns.Region, "Optioanl. The region of source vpc which will establish the intercome") + req.ProjectId = cmd.Flags().String("project-id", base.ConfigIns.ProjectID, "Optional. The project id of the source vpc") + req.DstProjectId = cmd.Flags().String("dst-project-id", base.ConfigIns.ProjectID, "Optional. The project id of the source vpc") + cmd.MarkFlagRequired("vpc-id") - cmd.MarkFlagRequired("dstvpc-id") - cmd.MarkFlagRequired("dstregion") + cmd.MarkFlagRequired("dst-vpc-id") + + cmd.Flags().SetFlagValuesFunc("vpc-id", func() []string { + return getAllVPCIdNames(*req.ProjectId, *req.Region) + }) + cmd.Flags().SetFlagValuesFunc("dst-vpc-id", func() []string { + return getAllVPCIdNames(*req.DstProjectId, *req.DstRegion) + }) + cmd.Flags().SetFlagValuesFunc("region", getRegionList) + cmd.Flags().SetFlagValuesFunc("dst-region", getRegionList) + cmd.Flags().SetFlagValuesFunc("project-id", getProjectList) + cmd.Flags().SetFlagValuesFunc("dst-project-id", getProjectList) return cmd } -//NewDescribeVPCIntercomRequest -func NewCmdVpcListPeer() *cobra.Command { - req := BizClient.NewDescribeVPCIntercomRequest() +// VPCIntercomRow 表格行 +type VPCIntercomRow struct { + VPCName string + ResourceID string + Segments string + ProjectID string + DstRegion string + Group string +} + +// NewCmdVpcListPeer ucloud vpc list-intercome +func NewCmdVpcListPeer(out io.Writer) *cobra.Command { + req := base.BizClient.NewDescribeVPCIntercomRequest() cmd := &cobra.Command{ Use: "list-intercome", Short: "list intercome ", Long: "list intercome", Example: "ucloud vpc list-intercome --vpc-id xx", Run: func(cmd *cobra.Command, args []string) { - resp, err := BizClient.DescribeVPCIntercom(req) + req.VPCId = sdk.String(base.PickResourceID(*req.VPCId)) + resp, err := base.BizClient.DescribeVPCIntercom(req) if err != nil { - HandleError(err) + base.HandleError(err) return } - if global.json { - PrintJSON(resp.DataSet) - } else { - list := make([]VPCIntercomRow, 0) - for _, VPCIntercom := range resp.DataSet { - row := VPCIntercomRow{} - row.ProjectId = VPCIntercom.ProjectId - row.Network = VPCIntercom.Network - row.DstRegion = VPCIntercom.DstRegion - row.Name = VPCIntercom.Name - row.VPCId = VPCIntercom.VPCId - row.Tag = VPCIntercom.Tag - } - PrintTable(list, []string{"ProjectId", "Network", "DstRegion", "Name", "VPCId", "Tag"}) + list := make([]VPCIntercomRow, 0) + for _, VPCIntercom := range resp.DataSet { + row := VPCIntercomRow{} + row.ProjectID = VPCIntercom.ProjectId + row.Segments = strings.Join(VPCIntercom.Network, ",") + row.DstRegion = VPCIntercom.DstRegion + row.VPCName = VPCIntercom.Name + row.ResourceID = VPCIntercom.VPCId + row.Group = VPCIntercom.Tag + list = append(list, row) } - + base.PrintList(list, out) }, } req.VPCId = cmd.Flags().String("vpc-id", "", "Required. The vpc id which you wnat to describe the information") - req.ProjectId = cmd.Flags().String("project-id", ConfigInstance.ProjectID, "Optional. The project id of source vpc") - req.Region = cmd.Flags().String("region", ConfigInstance.Region, "Optional, The region of source vpc") + req.ProjectId = cmd.Flags().String("project-id", base.ConfigIns.ProjectID, "Optional. The project id of source vpc") + req.Region = cmd.Flags().String("region", base.ConfigIns.Region, "Optional, The region of source vpc") + + cmd.Flags().SetFlagValuesFunc("vpc-id", func() []string { + return getAllVPCIdNames(*req.ProjectId, *req.Region) + }) + cmd.Flags().SetFlagValuesFunc("region", getRegionList) + cmd.Flags().SetFlagValuesFunc("project-id", getProjectList) + cmd.MarkFlagRequired("vpc-id") return cmd } -//NewDeleteVPCIntercomRequest +// NewCmdVpcDeletePeer ucloud vpc delete-intercome func NewCmdVpcDeletePeer() *cobra.Command { - req := BizClient.NewDeleteVPCIntercomRequest() + req := base.BizClient.NewDeleteVPCIntercomRequest() cmd := &cobra.Command{ Use: "delete-intercome", Short: "delete the vpc intercome", Long: "delete the vpc intercome", - Example: "ucloud vpc delete-intercome --vpc-id xxx --dstvpc-id xxx", + Example: "ucloud vpc delete-intercome --vpc-id xxx --dst-vpc-id xxx", Run: func(cmd *cobra.Command, args []string) { - resp, err := BizClient.DeleteVPCIntercom(req) + req.VPCId = sdk.String(base.PickResourceID(*req.VPCId)) + req.DstVPCId = sdk.String(base.PickResourceID(*req.DstVPCId)) + _, err := base.BizClient.DeleteVPCIntercom(req) if err != nil { - HandleError(err) + base.HandleError(err) return } - Cxt.Printf("The intercome [%s] was deleted successfully", resp) + base.Cxt.Printf("intercome [%s<-->%s] deleted\n", *req.VPCId, *req.DstVPCId) }, } cmd.Flags().SortFlags = false - req.VPCId = cmd.Flags().String("vpc-id", "", "Required. The source vpc id from which you want to disconnect") - req.DstVPCId = cmd.Flags().String("dstvpc-id", "", "Required. The target vpc which you want to disconnect with source vpc") - req.ProjectId = cmd.Flags().String("project-id", ConfigInstance.ProjectID, "Optional. The project id of source vpc") - req.Region = cmd.Flags().String("region", ConfigInstance.Region, "Optional. The region of source vpc from which you want to disconnect") + req.VPCId = cmd.Flags().String("vpc-id", "", "Required. Resource ID of source VPC to disconnect with destination VPC") + req.DstVPCId = cmd.Flags().String("dst-vpc-id", "", "Required. Resource ID of destination VPC to disconnect with source VPC") + req.ProjectId = cmd.Flags().String("project-id", base.ConfigIns.ProjectID, "Optional. The project id of source vpc") + req.Region = cmd.Flags().String("region", base.ConfigIns.Region, "Optional. The region of source vpc to disconnect") + req.DstRegion = cmd.Flags().String("dst-region", "", "Optional. The region of dest vpc to disconnect") + cmd.MarkFlagRequired("vpc-id") - cmd.MarkFlagRequired("dstvpc-id") + cmd.MarkFlagRequired("dst-vpc-id") + cmd.MarkFlagRequired("dst-region") + + cmd.Flags().SetFlagValuesFunc("vpc-id", func() []string { + return getAllVPCIdNames(*req.ProjectId, *req.Region) + }) + cmd.Flags().SetFlagValuesFunc("dst-region", getRegionList) + return cmd } -//NewCreateSubnetRequest ucloud subnet create +func getAllVPCIns(project, region string) ([]vpc.VPCInfo, error) { + req := base.BizClient.NewDescribeVPCRequest() + req.ProjectId = &project + req.Region = ®ion + resp, err := base.BizClient.DescribeVPC(req) + if err != nil { + return nil, err + } + return resp.DataSet, nil +} + +func getAllVPCIdNames(project, region string) []string { + vpcInsList, err := getAllVPCIns(project, region) + list := []string{} + if err != nil { + return nil + } + for _, vpc := range vpcInsList { + list = append(list, fmt.Sprintf("%s/%s", vpc.VPCId, vpc.Name)) + } + return list +} + +// NewCmdSubnet ucloud subnet +func NewCmdSubnet() *cobra.Command { + cmd := &cobra.Command{ + Use: "subnet", + Short: "List, create and delete subnet", + Long: "List, create and delete subnet", + Args: cobra.NoArgs, + } + out := base.Cxt.GetWriter() + cmd.AddCommand(NewCmdSubnetList(out)) + cmd.AddCommand(NewCmdSubnetCreate()) + cmd.AddCommand(NewCmdSubnetDelete(out)) + cmd.AddCommand(NewCmdSubnetListResource(out)) + + return cmd +} + +// SubnetRow 表格行 +type SubnetRow struct { + SubnetName string + ResourceID string + Group string + AffiliatedVPC string + NetworkSegment string + CreationTime string +} + +// NewCmdSubnetList ucloud subnet list +func NewCmdSubnetList(out io.Writer) *cobra.Command { + req := base.BizClient.NewDescribeSubnetRequest() + cmd := &cobra.Command{ + Use: "list", + Short: "List subnet", + Long: `List subnet`, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + resp, err := base.BizClient.DescribeSubnet(req) + if err != nil { + base.HandleError(err) + return + } + list := make([]SubnetRow, 0) + for _, sn := range resp.DataSet { + row := SubnetRow{} + row.SubnetName = sn.SubnetName + row.ResourceID = sn.SubnetId + row.Group = sn.Tag + row.AffiliatedVPC = fmt.Sprintf("%s/%s", sn.VPCId, sn.VPCName) + row.NetworkSegment = fmt.Sprintf("%s/%s", sn.Subnet, sn.Netmask) + row.CreationTime = base.FormatDate(sn.CreateTime) + list = append(list, row) + } + base.PrintList(list, out) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + req.Region = flags.String("region", base.ConfigIns.Region, "Optional. Region, see 'ucloud region'") + req.ProjectId = flags.String("project-id", base.ConfigIns.ProjectID, "Optional. Project-id, see 'ucloud project list'") + flags.StringSliceVar(&req.SubnetIds, "subnet-id", []string{}, "Optional. Multiple values separated by commas") + req.VPCId = flags.String("vpc-id", "", "Optional. Resource ID of VPC") + req.Tag = flags.String("group", "", "Optional. Group") + req.Offset = flags.Int("offset", 0, "Optional. Offset") + req.Limit = flags.Int("limit", 50, "Optional. Limit") + + return cmd +} + +// NewCmdSubnetCreate ucloud subnet create func NewCmdSubnetCreate() *cobra.Command { - req := BizClient.NewCreateSubnetRequest() + var segment *net.IPNet + req := base.BizClient.NewCreateSubnetRequest() cmd := &cobra.Command{ - Use: "create-subnet", + Use: "create", Short: "Create subnet of vpc network", Long: "Create subnet of vpc network", - Example: "ucloud subnet create --vpc-id --segment", + Example: "ucloud subnet create --vpc-id uvnet-vpcxid --name testName --segment 192.168.2.0/24", Run: func(cmd *cobra.Command, args []string) { - resp, err := BizClient.CreateSubnet(req) + ipMaskStrs := strings.SplitN(segment.String(), "/", 2) + req.Subnet = sdk.String(ipMaskStrs[0]) + mask, err := strconv.Atoi(ipMaskStrs[1]) + if err != nil { + base.HandleError(err) + return + } + req.Netmask = sdk.Int(mask) + req.VPCId = sdk.String(base.PickResourceID(*req.VPCId)) + resp, err := base.BizClient.CreateSubnet(req) if err != nil { - HandleError(err) + base.HandleError(err) return } - Cxt.Printf("Subnet : %v created successfully!\n", resp.SubnetId) + base.Cxt.Printf("subnet[%s] created\n", resp.SubnetId) }, } flags := cmd.Flags() flags.SortFlags = false req.VPCId = cmd.Flags().String("vpc-id", "", "Required. Assign the VPC network of the subnet") - req.Subnet = cmd.Flags().String("segment", "", "Required. Same as the vpc network") - req.Region = cmd.Flags().String("region", ConfigInstance.Region, "Optional. The region of the subnet") - req.ProjectId = cmd.Flags().String("project-id", ConfigInstance.ProjectID, "Optional. The project id of the subnet") - req.Netmask = cmd.Flags().Int("netmask", 24, "Optional. The number of the IPs, default is 24") - req.SubnetName = cmd.Flags().String("name", "Subnet", "Optional. The default is Subnet") - req.Tag = cmd.Flags().String("Group", "Default", "Optional. Business group") + segment = cmd.Flags().IPNet("segment", net.IPNet{}, "Required. Segment of subnet. For example '192.168.0.0/24'") + req.SubnetName = cmd.Flags().String("name", "Subnet", "Optional. Name of subnet to create") + req.Region = cmd.Flags().String("region", base.ConfigIns.Region, "Optional. The region of the subnet") + req.ProjectId = cmd.Flags().String("project-id", base.ConfigIns.ProjectID, "Optional. The project id of the subnet") + req.Tag = cmd.Flags().String("group", "", "Optional. Business group") + req.Remark = cmd.Flags().String("remark", "", "Optional. Remark of subnet to create") + + cmd.Flags().SetFlagValuesFunc("vpc-id", func() []string { + return getAllVPCIdNames(*req.ProjectId, *req.Region) + }) + cmd.MarkFlagRequired("vpc-id") cmd.MarkFlagRequired("segment") return cmd } + +// NewCmdSubnetDelete ucloud subnet delete +func NewCmdSubnetDelete(out io.Writer) *cobra.Command { + idNames := []string{} + req := base.BizClient.NewDeleteSubnetRequest() + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete subnet", + Long: "Delete subnet", + Run: func(c *cobra.Command, args []string) { + req.ProjectId = sdk.String(base.PickResourceID(*req.ProjectId)) + for _, id := range idNames { + req.SubnetId = sdk.String(base.PickResourceID(id)) + _, err := base.BizClient.DeleteSubnet(req) + if err != nil { + base.HandleError(err) + continue + } + fmt.Fprintf(out, "subnet[%s] deleted\n", id) + } + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&idNames, "subnet-id", nil, "Required. Resource ID of subent") + bindRegion(req, flags) + bindProjectID(req, flags) + cmd.MarkFlagRequired("subnet-id") + flags.SetFlagValuesFunc("subnet-id", func() []string { + return getAllSubnetIDNames("", *req.ProjectId, *req.Region) + }) + + return cmd +} + +// SubnetResourceRow 表格行 +type SubnetResourceRow struct { + ResourceName string + ResourceID string + ResourceType string + PrivateIP string +} + +// NewCmdSubnetListResource ucloud subnet list-resource +func NewCmdSubnetListResource(out io.Writer) *cobra.Command { + req := base.BizClient.NewDescribeSubnetResourceRequest() + cmd := &cobra.Command{ + Use: "list-resource", + Short: "List resources belong to subnet", + Long: "List resources belong to subnet", + Run: func(c *cobra.Command, args []string) { + req.SubnetId = sdk.String(base.PickResourceID(*req.SubnetId)) + resp, err := base.BizClient.DescribeSubnetResource(req) + if err != nil { + base.HandleError(err) + return + } + list := []SubnetResourceRow{} + for _, r := range resp.DataSet { + row := SubnetResourceRow{ + ResourceName: r.Name, + ResourceID: r.ResourceId, + ResourceType: r.ResourceType, + PrivateIP: r.IP, + } + list = append(list, row) + } + base.PrintList(list, out) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + req.SubnetId = flags.String("subnet-id", "", "Required. Resource ID of subnet which resources to list belong to") + req.ResourceType = flags.String("resource-type", "", "Optional. Resource type of resources to list. Accept values:'uhost','phost','ulb','uhadoophost','ufortresshost','unatgw','ukafka','umem','docker','udb','udw' and 'vip'") + bindRegion(req, flags) + bindProjectID(req, flags) + bindLimit(req, flags) + bindOffset(req, flags) + cmd.MarkFlagRequired("subnet-id") + flags.SetFlagValuesFunc("subnet-id", func() []string { + return getAllSubnetIDNames("", *req.ProjectId, *req.Region) + }) + flags.SetFlagValues("resource-type", "uhost", "phost", "ulb", "uhadoophost", "ufortresshost", "unatgw", "ukafka", "umem", "docker", "udb", "udw", "vip") + + return cmd +} + +func getAllSubnets(vpcID, project, region string) ([]vpc.SubnetInfo, error) { + req := base.BizClient.NewDescribeSubnetRequest() + req.ProjectId = sdk.String(base.PickResourceID(project)) + req.Region = sdk.String(region) + if vpcID != "" { + req.VPCId = sdk.String(base.PickResourceID(vpcID)) + } + subnets := []vpc.SubnetInfo{} + for limit, offset := 50, 0; ; offset += limit { + req.Limit = sdk.Int(limit) + req.Offset = sdk.Int(offset) + resp, err := base.BizClient.DescribeSubnet(req) + if err != nil { + base.HandleError(err) + return nil, err + } + subnets = append(subnets, resp.DataSet...) + if limit+offset >= resp.TotalCount { + break + } + } + return subnets, nil +} + +func getAllSubnetIDNames(vpcID, project, region string) []string { + subnets, err := getAllSubnets(vpcID, project, region) + if err != nil { + return nil + } + list := []string{} + for _, s := range subnets { + list = append(list, fmt.Sprintf("%s/%s", s.SubnetId, s.SubnetName)) + } + return list +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000..58b024d2b4 --- /dev/null +++ b/go.mod @@ -0,0 +1,32 @@ +module github.com/ucloud/ucloud-cli + +go 1.19 + +require ( + github.com/fatih/color v1.13.0 + github.com/satori/go.uuid v1.2.0 + github.com/sirupsen/logrus v1.3.0 + github.com/spf13/cobra v0.0.3 + github.com/spf13/pflag v1.0.3 + github.com/ucloud/ucloud-sdk-go v0.22.25 + golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 +) + +require ( + github.com/cpuguy83/go-md2man v1.0.10 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect + github.com/kr/pretty v0.1.0 // indirect + github.com/mattn/go-colorable v0.1.9 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/pkg/errors v0.8.0 // indirect + github.com/russross/blackfriday v1.5.2 // indirect + golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + gopkg.in/yaml.v2 v2.2.2 // indirect +) + +replace ( + github.com/spf13/cobra v0.0.3 => github.com/lixiaojun629/cobra v0.0.10 + github.com/spf13/pflag v1.0.3 => github.com/lixiaojun629/pflag v1.0.5 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000..587a20d04f --- /dev/null +++ b/go.sum @@ -0,0 +1,78 @@ +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lixiaojun629/cobra v0.0.10 h1:t/BZXAogMO3W4Y2OxcE/W8SOhfVcbSL0LwINfoYVFCI= +github.com/lixiaojun629/cobra v0.0.10/go.mod h1:6VKYqzoixuRlMBmzm3rHPS0sRYVhT3zXEfrt+Qf8eMs= +github.com/lixiaojun629/pflag v1.0.5 h1:plFJ2SBJd2S2Fc7ZwwFZ3682IvxBiUkhRuJS40OvEMs= +github.com/lixiaojun629/pflag v1.0.5/go.mod h1:uchrjsiFxJj1XOBpO4YJCZwpqXHsCHovxY91tyFoUrg= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME= +github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/ucloud/ucloud-sdk-go v0.22.17 h1:EFn+GxVKS5Tj8hIPie3qL6Zgk25fmWcHqJ06K8wl+Qo= +github.com/ucloud/ucloud-sdk-go v0.22.17/go.mod h1:dyLmFHmUfgb4RZKYQP9IArlvQ2pxzFthfhwxRzOEPIw= +github.com/ucloud/ucloud-sdk-go v0.22.25 h1:ceKeH7WFnpUt9nJSubn+mnxS1iKGrk/Q+HLwa0iYwmQ= +github.com/ucloud/ucloud-sdk-go v0.22.25/go.mod h1:dyLmFHmUfgb4RZKYQP9IArlvQ2pxzFthfhwxRzOEPIw= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/hack/github-release.sh b/hack/github-release.sh new file mode 100755 index 0000000000..2950ab0310 --- /dev/null +++ b/hack/github-release.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +set -ue + +targets=( \ + "darwin_amd64" \ + "darwin_arm64" \ + "linux_amd64" \ + "linux_arm64" \ + "windows_amd64" \ + "windows_arm64" \ +) + +VERSION=${GITHUB_REF#refs/*/} +echo "VERSION=${VERSION}" >> $GITHUB_ENV + +mkdir -p out + +for target in "${targets[@]}"; do + echo "Build target: ${target}" + IFS='_' read -r -a tmp <<< "$target" + BUILD_OS="${tmp[0]}" + BUILD_ARCH="${tmp[1]}" + GOOS="${BUILD_OS}" GOARCH="${BUILD_ARCH}" go build -mod=vendor -o bin/ucloud + zip -r out/ucloud-${target}.zip ./bin LICENSE +done diff --git a/main.go b/main.go index 9df448afa8..de8d691d0c 100644 --- a/main.go +++ b/main.go @@ -1,4 +1,4 @@ -// Copyright © 2018 NAME HERE +// Copyright © 2018 NAME HERE tony.li@ucloud.cn // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/model/cli/cli_const.go b/model/cli/cli_const.go new file mode 100644 index 0000000000..1aff3f907c --- /dev/null +++ b/model/cli/cli_const.go @@ -0,0 +1,8 @@ +package cli + +const IMAGE_BASE = "Base" +const IMAGE_BUSINESS = "Business" +const IAMGE_CUSTOM = "Custom" +const IMAGE_ALL = "*" + +const REGEXP_NAME = "^[A-Za-z0-9-_.\u4e00-\u9fa5]{1,63}$" diff --git a/model/context.go b/model/context.go index d02076e84f..d41e3019fd 100644 --- a/model/context.go +++ b/model/context.go @@ -15,37 +15,37 @@ type Context struct { data map[string]interface{} } -//Print 打印一行 +// Print 打印一行 func (c *Context) Print(a ...interface{}) (n int, err error) { text := fmt.Sprint(a...) n, err = c.writer.Write([]byte(text)) return } -//Println 打印一行 +// Println 打印一行 func (c *Context) Println(a ...interface{}) (n int, err error) { text := fmt.Sprintln(a...) n, err = c.writer.Write([]byte(text)) return } -//Printf 根据格式字符串打印 +// Printf 根据格式字符串打印 func (c *Context) Printf(format string, a ...interface{}) (n int, err error) { text := fmt.Sprintf(format, a...) n, err = c.writer.Write([]byte(text)) return } -//PrintErr 打印错误 +// PrintErr 打印错误 func (c *Context) PrintErr(uerr error) (n int, err error) { text := fmt.Sprintf("Error:%v\n", uerr) n, err = c.writer.Write([]byte(text)) return } -//AppendInfo 添加记录 -func (c *Context) AppendInfo(key string, content interface{}) { - c.data[key] = content +// GetWriter 获取Writer +func (c *Context) GetWriter() io.Writer { + return c.writer } // GetContext 创建一个单例的Context diff --git a/model/context_test.go b/model/context_test.go index 066e220798..203b7606e4 100644 --- a/model/context_test.go +++ b/model/context_test.go @@ -7,7 +7,7 @@ import ( ) var ctx = Context{ - os.Stdout, + writer: os.Stdout, } func TestPrintln(t *testing.T) { diff --git a/model/status/status.go b/model/status/status.go index 34431a2fb2..8040020162 100644 --- a/model/status/status.go +++ b/model/status/status.go @@ -4,6 +4,33 @@ const HOST_RUNNING = "Running" const HOST_STOPPED = "Stopped" const HOST_FAIL = "Install Fail" +const IMAGE_MAKING = "Making" +const IMAGE_AVAILABLE = "Available" +const IMAGE_UNAVAILABLE = "Unavailable" +const IMAGE_COPYING = "Copying" + const DISK_INUSE = "InUse" const DISK_AVAILABLE = "Available" const DISK_FAILED = "Failed" +const DISK_RESTORING = "Restoring" + +const UDB_INIT = "Init" +const UDB_FAIL = "Fail" +const UDB_RUNNING = "Running" +const UDB_SHUTOFF = "Shutoff" +const UDB_DELETE = "Delete" +const UDB_RECOVER_FAIL = "Recover fail" +const UDB_UPGRADE_FAIL = "UpgradeFail" +const UDB_TOBE_SWITCH = "WaitForSwitch" + +const SNAPSHOT_NORMAL = "Normal" + +const EIP_FREE = "free" +const EIP_USED = "used" + +const EIP_CHARGE_BANDWIDTH = "Bandwidth" +const EIP_CHARGE_TRAFFIC = "Traffic" +const EIP_CHARGE_SHARE = "ShareBandwidth" + +const UMEM_FAIL = "Fail" +const UMEM_RUNNING = "Running" diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000000..c6db60694d --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,9 @@ +# Gauge - metadata dir +.gauge + +# Gauge - log files dir +logs + +# Gauge - reports generated by reporting plugins +reports + diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000000..5176ce5588 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,9 @@ +# UCloud CLI Examples & BDD Test Case + +BDD test cases for CLI. + +## QuickStart + +1. `brew install gauge` +2. `gauge install go` +3. `gauge run specs/` diff --git a/tests/env/default/default.properties b/tests/env/default/default.properties new file mode 100644 index 0000000000..00b43aee18 --- /dev/null +++ b/tests/env/default/default.properties @@ -0,0 +1,28 @@ +# default.properties +# properties set here will be available to the test execution as environment variables + +# sample_key = sample_value + +# The path to the gauge reports directory. Should be either relative to the project directory or an absolute path +gauge_reports_dir = reports + +# Set as false if gauge reports should not be overwritten on each execution. A new time-stamped directory will be created on each execution. +overwrite_reports = true + +# Set to false to disable screenshots on failure in reports. +screenshot_on_failure = true + +# The path to the gauge logs directory. Should be either relative to the project directory or an absolute path +logs_directory = logs + +# Set to true to use multithreading for parallel execution +enable_multithreading = false + +# The path the gauge specifications directory. Takes a comma separated list of specification files/directories. +gauge_specs_dir = specs + +# The default delimiter used read csv files. +csv_delimiter = , + +# Allows steps to be written in multiline +allow_multiline_step = false diff --git a/tests/go.mod b/tests/go.mod new file mode 100644 index 0000000000..286e4bfa14 --- /dev/null +++ b/tests/go.mod @@ -0,0 +1,5 @@ +module tests + +go 1.16 + +require github.com/getgauge-contrib/gauge-go v0.2.0 diff --git a/tests/go.sum b/tests/go.sum new file mode 100644 index 0000000000..e192097a9c --- /dev/null +++ b/tests/go.sum @@ -0,0 +1,43 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dmotylev/goproperties v0.0.0-20140630191356-7cbffbaada47 h1:sP2APvSdZpfBiousrppBZNOvu+TE79Myq4kkmmrtSuI= +github.com/dmotylev/goproperties v0.0.0-20140630191356-7cbffbaada47/go.mod h1:f2V6964+f0p8Asqy8mIK5cKyyVc6MP9PFzGVNRcnYJQ= +github.com/getgauge-contrib/gauge-go v0.2.0 h1:UkqEXm+APHC2aswqC9cG9qwEuXNanId/hsOuGiiMx08= +github.com/getgauge-contrib/gauge-go v0.2.0/go.mod h1:6/Aagmhzq0I878fDXqIkrnzvo3aaTuJ5upVhOQTGL48= +github.com/getgauge/common v0.0.0-20160906120419-fce5f398028f h1:VoshOiFVQ0XJMeRh7uYkZc9gkBiIJzztt9ZR+bCB4kY= +github.com/getgauge/common v0.0.0-20160906120419-fce5f398028f/go.mod h1:tHtGp+rvfECQYRDCNQX46uQTO6qHpdDrikX9ZkBjkSA= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tests/manifest.json b/tests/manifest.json new file mode 100644 index 0000000000..738d20e871 --- /dev/null +++ b/tests/manifest.json @@ -0,0 +1,6 @@ +{ + "Language": "go", + "Plugins": [ + "html-report" + ] +} diff --git a/tests/specs/pathx.spec b/tests/specs/pathx.spec new file mode 100644 index 0000000000..a376e94209 --- /dev/null +++ b/tests/specs/pathx.spec @@ -0,0 +1,23 @@ +# UCloud PathX Example Test + + +## Create PathX instance with port + +* Extract "id" by regexp("ID is: ([^\s]+)"): "ucloud pathx create --bandwidth 1 --area-code CAN --charge-type Dynamic --quantity 1 --accel Global --origin-domain www.ucloud.cn --port 8000-8001 --origin-port 8000-8001 --protocol TCP" +* Execute command: "ucloud pathx list" +* Execute command with "id": "ucloud pathx list --id $id" +* Execute command with "id": "ucloud pathx list --id $id --detail" +* Execute command: "ucloud pathx price list --bandwidth 10 --area-code BKK" +* Execute command: "ucloud pathx area list" +* Execute command: "ucloud pathx area list --origin-domain www.ucloud.cn" +* Execute command: "ucloud pathx area list --origin-domain www.ucloud.cn --no-accel" +* Execute command: "ucloud pathx area list --origin-domain www.ucloud.cn --accel Global" +* Execute command with "id": "ucloud pathx delete -y --id $id" + +## Create PathX instance without port + +* Extract "id" by regexp("ID is: ([^\s]+)"): "ucloud pathx create --bandwidth 1 --area-code BKK --charge-type Dynamic --quantity 1 --accel AP --origin-domain www.ucloud.cn" +* Execute command with "id": "ucloud pathx modify --bandwidth 2 --id $id" +* Execute command with "id": "ucloud pathx modify --origin-domain pathx.ucloud.cn --id $id" +* Execute command with "id": "ucloud pathx modify --name PathX产品测试 --remark 测试 --id $id" +* Execute command with "id": "ucloud pathx delete -y --id $id" diff --git a/tests/stepimpl/stepimpl.go b/tests/stepimpl/stepimpl.go new file mode 100644 index 0000000000..3e109e56ba --- /dev/null +++ b/tests/stepimpl/stepimpl.go @@ -0,0 +1,47 @@ +package stepImpl + +import ( + "fmt" + "os/exec" + "regexp" + "strings" + + "github.com/getgauge-contrib/gauge-go/gauge" + "github.com/getgauge-contrib/gauge-go/testsuit" +) + +var _ = gauge.Step(`Execute command: `, func(command string) { + _ = execCmd(command) +}) + +var _ = gauge.Step(`Extract by regexp(): `, func(variable, pattern, command string) { + out := execCmd(command) + matched := regexp.MustCompile(pattern).FindStringSubmatch(string(out)) + if len(matched) < 2 { + testsuit.T.Fail(fmt.Errorf("no matched for %s: %s", pattern, string(out))) + } + gauge.GetScenarioStore()[variable] = matched[1] +}) + +var _ = gauge.Step(`Execute command with : `, func(variable, command string) { + _ = execCmd(strings.ReplaceAll(command, "$"+variable, fmt.Sprint(gauge.GetScenarioStore()[variable]))) +}) + +func execCmd(command string) []byte { + cmd := newCmd(command) + out, err := cmd.CombinedOutput() + gauge.WriteMessage(string(out)) + if err != nil { + testsuit.T.Fail(fmt.Errorf("cmd.Run() failed with %s\n", err)) + } + return out +} + +func newCmd(command string) *exec.Cmd { + tokens := strings.Split(command, " ") + binary, err := exec.LookPath(tokens[0]) + if err != nil { + testsuit.T.Fail(fmt.Errorf("can not found binary: %s", binary)) + } + return exec.Command(binary, tokens[1:]...) +} diff --git a/ux/document.go b/ux/document.go new file mode 100644 index 0000000000..fcb59301ed --- /dev/null +++ b/ux/document.go @@ -0,0 +1,198 @@ +package ux + +import ( + "fmt" + "io" + "os" + "sync" + "time" + + "github.com/ucloud/ucloud-cli/ansi" +) + +var width, rows, _ = terminalSize() + +// Document 当前进程在打印的内容 +type document struct { + blocks []*Block + mux sync.RWMutex + framesPerSecond int + once sync.Once + out io.Writer + ticker *time.Ticker + disable bool +} + +func (d *document) reset() { + size := 0 + d.mux.RLock() + for _, block := range d.blocks { + size += block.printLineNum + } + d.mux.RUnlock() + if size != 0 { + fmt.Printf(ansi.CursorLeft + ansi.CursorPrevLine(size) + ansi.EraseDown) + } +} + +func (d *document) Disable() { + d.disable = true +} + +func (d *document) SetWriter(out io.Writer) { + d.out = out +} + +func (d *document) Content() []string { + var lines []string + for _, block := range d.blocks { + for _, line := range <-block.getLines { + lines = append(lines, line) + } + } + return lines +} + +func (d *document) Render() { + if d.disable { + return + } + d.once.Do(func() { + go func() { + for range d.ticker.C { + d.reset() + d.mux.RLock() + for _, block := range d.blocks { + block.printLineNum = 0 + for _, line := range <-block.getLines { + fmt.Fprintln(d.out, line) + if width != 0 { + lineNum := len(line)/width + 1 + block.printLineNum += lineNum + } else { + block.printLineNum++ + } + } + fmt.Fprintf(d.out, "\n") + block.printLineNum++ + } + d.mux.RUnlock() + } + }() + }) +} + +func (d *document) Append(b *Block) { + d.Render() + d.mux.Lock() + defer d.mux.Unlock() + d.blocks = append(d.blocks, b) +} + +func (d *document) GetLastBlock() *Block { + d.mux.Lock() + defer d.mux.Unlock() + if len(d.blocks) == 0 { + return nil + } + return d.blocks[len(d.blocks)-1] +} + +func (d *document) GetBlockCount() int { + d.mux.Lock() + defer d.mux.Unlock() + return len(d.blocks) +} + +func newDocument(out io.Writer) *document { + doc := &document{ + out: out, + framesPerSecond: 20, + } + doc.ticker = time.NewTicker(time.Second / time.Duration(doc.framesPerSecond)) + return doc +} + +// Doc global document +var Doc = newDocument(os.Stdout) + +// Block in document, including a spinner and some text +type Block struct { + spinner *Spin + spinnerIndex int + printLineNum int //已打印到屏幕上的行数 + lines []string + updateLine chan updateBlockLine + getLines chan []string +} + +// Update lines in Block +func (b *Block) Update(text string, index int) { + b.updateLine <- updateBlockLine{text, index} +} + +// Append text to Block +func (b *Block) Append(text string) { + b.updateLine <- updateBlockLine{text, -1} +} + +// SetSpin set spin for block +func (b *Block) SetSpin(s *Spin) error { + if b.spinner != nil { + return fmt.Errorf("block has spinner already") + } + b.spinner = s + b.spinnerIndex = len(<-b.getLines) + strsCh := b.spinner.renderToString() + go func() { + for text := range strsCh { + if len(<-b.getLines) == 0 { + b.Append(text) + } else { + b.Update(text, b.spinnerIndex) + } + } + }() + return nil +} + +type updateBlockLine struct { + line string + index int +} + +// NewSpinBlock create a new Block with spinner +func NewSpinBlock(s *Spin) *Block { + block := NewBlock() + if s != nil { + block.SetSpin(s) + } + return block +} + +// NewBlock create a new Block without spinner. block.Stable closed +func NewBlock() *Block { + block := &Block{ + lines: []string{}, + updateLine: make(chan updateBlockLine, 0), + getLines: make(chan []string, 0), + } + + go func() { + for { + select { + case updateLine := <-block.updateLine: + index, line := updateLine.index, updateLine.line + if index < 0 { + block.lines = append(block.lines, line) + } else { + block.lines[index] = line + } + + case block.getLines <- block.lines: + } + } + }() + + return block +} diff --git a/ux/prompt.go b/ux/prompt.go index ba2a93cae2..045d38384b 100644 --- a/ux/prompt.go +++ b/ux/prompt.go @@ -3,8 +3,6 @@ package ux import ( "fmt" "strings" - - "github.com/ucloud/ucloud-cli/base" ) // Prompt confirm @@ -12,7 +10,7 @@ func Prompt(text string) (bool, error) { if !strings.HasSuffix(text, "(y/n):") { text += " (y/n):" } - base.Cxt.Printf(text) + fmt.Printf(text) var agreeClose string _, err := fmt.Scanf("%s\n", &agreeClose) if err != nil { diff --git a/ux/spinner.go b/ux/spinner.go index 0d268983af..56ac43f45f 100644 --- a/ux/spinner.go +++ b/ux/spinner.go @@ -4,22 +4,29 @@ package ux import ( "fmt" + "io" + "os" + "runtime" "time" "github.com/ucloud/ucloud-cli/ansi" ) +const windows = "windows" + // Spinner type type Spinner struct { + out io.Writer frames []rune framesPerSecond int DoingText string DoneText string + TimeoutText string ticker *time.Ticker output string } -// Start start rendor +// Start start render func (s *Spinner) Start(doingText string) { if doingText != "" { s.DoingText = doingText @@ -28,12 +35,28 @@ func (s *Spinner) Start(doingText string) { s.render() } -// Stop stop rendor +// Stop stop render func (s *Spinner) Stop() { s.ticker.Stop() s.reset() output := fmt.Sprintf("%s...%s\n", s.DoingText, s.DoneText) - fmt.Printf(output) + fmt.Fprintf(s.out, output) +} + +// Timeout stop render +func (s *Spinner) Timeout() { + s.ticker.Stop() + s.reset() + output := fmt.Sprintf("%s...%s\n", s.DoingText, s.TimeoutText) + fmt.Fprintf(s.out, output) +} + +// Fail stop render +func (s *Spinner) Fail(err error) { + s.ticker.Stop() + s.reset() + output := fmt.Sprintf("%s...fail: %v\n", s.DoingText, err) + fmt.Fprintf(s.out, output) } func (s *Spinner) reset() { @@ -47,7 +70,15 @@ func (s *Spinner) reset() { func (s *Spinner) render() { nextFrame := s.newFrameFactory() go func() { + send := false for range s.ticker.C { + if runtime.GOOS == windows { + if !send { + fmt.Printf("%s...\n", s.DoingText) + send = true + } + continue + } frame := nextFrame() s.reset() s.output = fmt.Sprintf("%s...%c\n", s.DoingText, frame) @@ -69,9 +100,39 @@ func (s *Spinner) newFrameFactory() func() rune { var spinnerFrames = []rune{'⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'} // DotSpinner dot spinner -var DotSpinner = &Spinner{frames: spinnerFrames, framesPerSecond: 12, DoingText: "running", DoneText: "done"} +var DotSpinner = NewDotSpinner(os.Stdout) -//NewDotSpinner get new DotSpinner instance -func NewDotSpinner() *Spinner { - return &Spinner{frames: spinnerFrames, framesPerSecond: 12, DoingText: "running", DoneText: "done"} +// NewDotSpinner get new DotSpinner instance +func NewDotSpinner(out io.Writer) *Spinner { + return &Spinner{ + out: out, + frames: spinnerFrames, + framesPerSecond: 12, + DoingText: "running", + DoneText: "done", + TimeoutText: "timeout", + } +} + +// Refresh 刷新显示文本 +type Refresh struct { + out io.Writer + reset bool +} + +// Do 刷新显示 +func (r *Refresh) Do(text string) { + if r.reset { + fmt.Fprintf(r.out, ansi.CursorLeft+ansi.CursorUp(1)+ansi.EraseDown) + } else { + r.reset = true + } + fmt.Fprintln(r.out, text) +} + +// NewRefresh create a new Refresh instance +func NewRefresh() *Refresh { + return &Refresh{ + out: os.Stdout, + } } diff --git a/ux/spinnerv2.go b/ux/spinnerv2.go new file mode 100644 index 0000000000..5469bf87fa --- /dev/null +++ b/ux/spinnerv2.go @@ -0,0 +1,123 @@ +//Inspaired by https://github.com/oclif/cli-ux + +package ux + +import ( + "fmt" + "io" + "runtime" + "sync" + "time" + + "github.com/ucloud/ucloud-cli/ansi" +) + +// Spin type +type Spin struct { + out io.Writer + frames []rune + framesPerSecond int + DoingText string + DoneText string + TimeoutText string + ticker *time.Ticker + output string + textChan chan string + wg sync.WaitGroup +} + +// Stop stop render +func (s *Spin) Stop() { + s.ticker.Stop() + s.reset() + output := fmt.Sprintf("%s...%s", s.DoingText, s.DoneText) + s.textChan <- output + //等待最后一帧渲染 + <-time.After(time.Millisecond * 100) + close(s.textChan) +} + +// Timeout stop render +func (s *Spin) Timeout() { + s.ticker.Stop() + s.reset() + output := fmt.Sprintf("%s...%s", s.DoingText, s.TimeoutText) + s.textChan <- output + //等待最后一帧渲染 + <-time.After(time.Millisecond * 100) + close(s.textChan) +} + +func (s *Spin) reset() { + if s.output == "" { + return + } + fmt.Printf(ansi.CursorLeft + ansi.CursorUp(1) + ansi.EraseDown) + s.output = "" +} + +func (s *Spin) renderToString() chan string { + nextFrame := s.newFrameFactory() + go func() { + send := false + for range s.ticker.C { + if runtime.GOOS == windows { + if !send { + s.textChan <- fmt.Sprintf("%s...", s.DoingText) + send = true + } + continue + } + frame := nextFrame() + s.textChan <- fmt.Sprintf("%s...%c", s.DoingText, frame) + } + }() + return s.textChan +} + +func (s *Spin) renderToScreen() { + nextFrame := s.newFrameFactory() + go func() { + send := false + for range s.ticker.C { + if runtime.GOOS == windows { + if !send { + fmt.Printf("%s...\n", s.DoingText) + send = true + } + continue + } + frame := nextFrame() + s.reset() + s.output = fmt.Sprintf("%s...%c\n", s.DoingText, frame) + fmt.Printf(s.output) + } + }() +} + +func (s *Spin) newFrameFactory() func() rune { + index := 0 + size := len(s.frames) + return func() rune { + char := s.frames[index%size] + index++ + return char + } +} + +var spinFrames = []rune{'⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'} + +// NewDotSpin get new DotSpinner instance +func NewDotSpin(out io.Writer, doingText string) *Spin { + s := &Spin{ + out: out, + frames: spinnerFrames, + framesPerSecond: 12, + DoingText: doingText, + DoneText: "done", + TimeoutText: "timeout", + textChan: make(chan string), + } + s.ticker = time.NewTicker(time.Second / time.Duration(s.framesPerSecond)) + return s +} diff --git a/ux/terminal_win.go b/ux/terminal_win.go new file mode 100644 index 0000000000..ba9b9d5171 --- /dev/null +++ b/ux/terminal_win.go @@ -0,0 +1,77 @@ +//go:build windows +// +build windows + +package ux + +import ( + "os" + "syscall" + "unsafe" +) + +var tty = os.Stdin + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + + // GetConsoleScreenBufferInfo retrieves information about the + // specified console screen buffer. + // http://msdn.microsoft.com/en-us/library/windows/desktop/ms683171(v=vs.85).aspx + procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") + + // GetConsoleMode retrieves the current input mode of a console's + // input buffer or the current output mode of a console screen buffer. + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms683167(v=vs.85).aspx + getConsoleMode = kernel32.NewProc("GetConsoleMode") + + // SetConsoleMode sets the input mode of a console's input buffer + // or the output mode of a console screen buffer. + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx + setConsoleMode = kernel32.NewProc("SetConsoleMode") + + // SetConsoleCursorPosition sets the cursor position in the + // specified console screen buffer. + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms686025(v=vs.85).aspx + setConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition") +) + +type ( + // Defines the coordinates of the upper left and lower right corners + // of a rectangle. + // See + // http://msdn.microsoft.com/en-us/library/windows/desktop/ms686311(v=vs.85).aspx + smallRect struct { + Left, Top, Right, Bottom int16 + } + + // Defines the coordinates of a character cell in a console screen + // buffer. The origin of the coordinate system (0,0) is at the top, left cell + // of the buffer. + // See + // http://msdn.microsoft.com/en-us/library/windows/desktop/ms682119(v=vs.85).aspx + coordinates struct { + X, Y int16 + } + + word int16 + + // Contains information about a console screen buffer. + // http://msdn.microsoft.com/en-us/library/windows/desktop/ms682093(v=vs.85).aspx + consoleScreenBufferInfo struct { + dwSize coordinates + dwCursorPosition coordinates + wAttributes word + srWindow smallRect + dwMaximumWindowSize coordinates + } +) + +// terminalSize returns width ans rows of the terminal. +func terminalSize() (int, int, error) { + var info consoleScreenBufferInfo + _, _, e := syscall.Syscall(procGetConsoleScreenBufferInfo.Addr(), 2, uintptr(syscall.Stdout), uintptr(unsafe.Pointer(&info)), 0) + if e != 0 { + return 0, 0, error(e) + } + return int(info.dwSize.X) - 1, int(info.dwSize.Y) - 1, nil +} diff --git a/ux/terminal_x.go b/ux/terminal_x.go new file mode 100644 index 0000000000..3d71bcc4be --- /dev/null +++ b/ux/terminal_x.go @@ -0,0 +1,50 @@ +//go:build linux || darwin || freebsd || netbsd || openbsd || solaris || dragonfly +// +build linux darwin freebsd netbsd openbsd solaris dragonfly + +package ux + +import ( + "errors" + "os" + "sync" + + "golang.org/x/sys/unix" +) + +var ( + echoLockMutex sync.Mutex + origTermStatePtr *unix.Termios + tty *os.File + istty bool +) + +func init() { + echoLockMutex.Lock() + defer echoLockMutex.Unlock() + + var err error + tty, err = os.Open("/dev/tty") + istty = true + if err != nil { + tty = os.Stdin + istty = false + } +} + +// terminalSize returns width and rows of the terminal. +func terminalSize() (int, int, error) { + if !istty { + return 0, 0, errors.New("Not Supported") + } + echoLockMutex.Lock() + defer echoLockMutex.Unlock() + + fd := int(tty.Fd()) + + ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) + if err != nil { + return 0, 0, err + } + + return int(ws.Col), int(ws.Row), nil +} diff --git a/vendor/github.com/Sirupsen/logrus/terminal_bsd.go b/vendor/github.com/Sirupsen/logrus/terminal_bsd.go deleted file mode 100644 index 5b6212d24c..0000000000 --- a/vendor/github.com/Sirupsen/logrus/terminal_bsd.go +++ /dev/null @@ -1,10 +0,0 @@ -// +build darwin freebsd openbsd netbsd dragonfly -// +build !appengine,!js - -package logrus - -import "golang.org/x/sys/unix" - -const ioctlReadTermios = unix.TIOCGETA - -type Termios unix.Termios diff --git a/vendor/github.com/cpuguy83/go-md2man/LICENSE.md b/vendor/github.com/cpuguy83/go-md2man/LICENSE.md new file mode 100644 index 0000000000..1cade6cef6 --- /dev/null +++ b/vendor/github.com/cpuguy83/go-md2man/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Brian Goff + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/cpuguy83/go-md2man/md2man/md2man.go b/vendor/github.com/cpuguy83/go-md2man/md2man/md2man.go new file mode 100644 index 0000000000..af62279a61 --- /dev/null +++ b/vendor/github.com/cpuguy83/go-md2man/md2man/md2man.go @@ -0,0 +1,20 @@ +package md2man + +import ( + "github.com/russross/blackfriday" +) + +// Render converts a markdown document into a roff formatted document. +func Render(doc []byte) []byte { + renderer := RoffRenderer(0) + extensions := 0 + extensions |= blackfriday.EXTENSION_NO_INTRA_EMPHASIS + extensions |= blackfriday.EXTENSION_TABLES + extensions |= blackfriday.EXTENSION_FENCED_CODE + extensions |= blackfriday.EXTENSION_AUTOLINK + extensions |= blackfriday.EXTENSION_SPACE_HEADERS + extensions |= blackfriday.EXTENSION_FOOTNOTES + extensions |= blackfriday.EXTENSION_TITLEBLOCK + + return blackfriday.Markdown(doc, renderer, extensions) +} diff --git a/vendor/github.com/cpuguy83/go-md2man/md2man/roff.go b/vendor/github.com/cpuguy83/go-md2man/md2man/roff.go new file mode 100644 index 0000000000..8c29ec6873 --- /dev/null +++ b/vendor/github.com/cpuguy83/go-md2man/md2man/roff.go @@ -0,0 +1,285 @@ +package md2man + +import ( + "bytes" + "fmt" + "html" + "strings" + + "github.com/russross/blackfriday" +) + +type roffRenderer struct { + ListCounters []int +} + +// RoffRenderer creates a new blackfriday Renderer for generating roff documents +// from markdown +func RoffRenderer(flags int) blackfriday.Renderer { + return &roffRenderer{} +} + +func (r *roffRenderer) GetFlags() int { + return 0 +} + +func (r *roffRenderer) TitleBlock(out *bytes.Buffer, text []byte) { + out.WriteString(".TH ") + + splitText := bytes.Split(text, []byte("\n")) + for i, line := range splitText { + line = bytes.TrimPrefix(line, []byte("% ")) + if i == 0 { + line = bytes.Replace(line, []byte("("), []byte("\" \""), 1) + line = bytes.Replace(line, []byte(")"), []byte("\" \""), 1) + } + line = append([]byte("\""), line...) + line = append(line, []byte("\" ")...) + out.Write(line) + } + out.WriteString("\n") + + // disable hyphenation + out.WriteString(".nh\n") + // disable justification (adjust text to left margin only) + out.WriteString(".ad l\n") +} + +func (r *roffRenderer) BlockCode(out *bytes.Buffer, text []byte, lang string) { + out.WriteString("\n.PP\n.RS\n\n.nf\n") + escapeSpecialChars(out, text) + out.WriteString("\n.fi\n.RE\n") +} + +func (r *roffRenderer) BlockQuote(out *bytes.Buffer, text []byte) { + out.WriteString("\n.PP\n.RS\n") + out.Write(text) + out.WriteString("\n.RE\n") +} + +func (r *roffRenderer) BlockHtml(out *bytes.Buffer, text []byte) { // nolint: golint + out.Write(text) +} + +func (r *roffRenderer) Header(out *bytes.Buffer, text func() bool, level int, id string) { + marker := out.Len() + + switch { + case marker == 0: + // This is the doc header + out.WriteString(".TH ") + case level == 1: + out.WriteString("\n\n.SH ") + case level == 2: + out.WriteString("\n.SH ") + default: + out.WriteString("\n.SS ") + } + + if !text() { + out.Truncate(marker) + return + } +} + +func (r *roffRenderer) HRule(out *bytes.Buffer) { + out.WriteString("\n.ti 0\n\\l'\\n(.lu'\n") +} + +func (r *roffRenderer) List(out *bytes.Buffer, text func() bool, flags int) { + marker := out.Len() + r.ListCounters = append(r.ListCounters, 1) + out.WriteString("\n.RS\n") + if !text() { + out.Truncate(marker) + return + } + r.ListCounters = r.ListCounters[:len(r.ListCounters)-1] + out.WriteString("\n.RE\n") +} + +func (r *roffRenderer) ListItem(out *bytes.Buffer, text []byte, flags int) { + if flags&blackfriday.LIST_TYPE_ORDERED != 0 { + out.WriteString(fmt.Sprintf(".IP \"%3d.\" 5\n", r.ListCounters[len(r.ListCounters)-1])) + r.ListCounters[len(r.ListCounters)-1]++ + } else { + out.WriteString(".IP \\(bu 2\n") + } + out.Write(text) + out.WriteString("\n") +} + +func (r *roffRenderer) Paragraph(out *bytes.Buffer, text func() bool) { + marker := out.Len() + out.WriteString("\n.PP\n") + if !text() { + out.Truncate(marker) + return + } + if marker != 0 { + out.WriteString("\n") + } +} + +func (r *roffRenderer) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) { + out.WriteString("\n.TS\nallbox;\n") + + maxDelims := 0 + lines := strings.Split(strings.TrimRight(string(header), "\n")+"\n"+strings.TrimRight(string(body), "\n"), "\n") + for _, w := range lines { + curDelims := strings.Count(w, "\t") + if curDelims > maxDelims { + maxDelims = curDelims + } + } + out.Write([]byte(strings.Repeat("l ", maxDelims+1) + "\n")) + out.Write([]byte(strings.Repeat("l ", maxDelims+1) + ".\n")) + out.Write(header) + if len(header) > 0 { + out.Write([]byte("\n")) + } + + out.Write(body) + out.WriteString("\n.TE\n") +} + +func (r *roffRenderer) TableRow(out *bytes.Buffer, text []byte) { + if out.Len() > 0 { + out.WriteString("\n") + } + out.Write(text) +} + +func (r *roffRenderer) TableHeaderCell(out *bytes.Buffer, text []byte, align int) { + if out.Len() > 0 { + out.WriteString("\t") + } + if len(text) == 0 { + text = []byte{' '} + } + out.Write([]byte("\\fB\\fC" + string(text) + "\\fR")) +} + +func (r *roffRenderer) TableCell(out *bytes.Buffer, text []byte, align int) { + if out.Len() > 0 { + out.WriteString("\t") + } + if len(text) > 30 { + text = append([]byte("T{\n"), text...) + text = append(text, []byte("\nT}")...) + } + if len(text) == 0 { + text = []byte{' '} + } + out.Write(text) +} + +func (r *roffRenderer) Footnotes(out *bytes.Buffer, text func() bool) { + +} + +func (r *roffRenderer) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) { + +} + +func (r *roffRenderer) AutoLink(out *bytes.Buffer, link []byte, kind int) { + out.WriteString("\n\\[la]") + out.Write(link) + out.WriteString("\\[ra]") +} + +func (r *roffRenderer) CodeSpan(out *bytes.Buffer, text []byte) { + out.WriteString("\\fB\\fC") + escapeSpecialChars(out, text) + out.WriteString("\\fR") +} + +func (r *roffRenderer) DoubleEmphasis(out *bytes.Buffer, text []byte) { + out.WriteString("\\fB") + out.Write(text) + out.WriteString("\\fP") +} + +func (r *roffRenderer) Emphasis(out *bytes.Buffer, text []byte) { + out.WriteString("\\fI") + out.Write(text) + out.WriteString("\\fP") +} + +func (r *roffRenderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) { +} + +func (r *roffRenderer) LineBreak(out *bytes.Buffer) { + out.WriteString("\n.br\n") +} + +func (r *roffRenderer) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) { + out.Write(content) + r.AutoLink(out, link, 0) +} + +func (r *roffRenderer) RawHtmlTag(out *bytes.Buffer, tag []byte) { // nolint: golint + out.Write(tag) +} + +func (r *roffRenderer) TripleEmphasis(out *bytes.Buffer, text []byte) { + out.WriteString("\\s+2") + out.Write(text) + out.WriteString("\\s-2") +} + +func (r *roffRenderer) StrikeThrough(out *bytes.Buffer, text []byte) { +} + +func (r *roffRenderer) FootnoteRef(out *bytes.Buffer, ref []byte, id int) { + +} + +func (r *roffRenderer) Entity(out *bytes.Buffer, entity []byte) { + out.WriteString(html.UnescapeString(string(entity))) +} + +func (r *roffRenderer) NormalText(out *bytes.Buffer, text []byte) { + escapeSpecialChars(out, text) +} + +func (r *roffRenderer) DocumentHeader(out *bytes.Buffer) { +} + +func (r *roffRenderer) DocumentFooter(out *bytes.Buffer) { +} + +func needsBackslash(c byte) bool { + for _, r := range []byte("-_&\\~") { + if c == r { + return true + } + } + return false +} + +func escapeSpecialChars(out *bytes.Buffer, text []byte) { + for i := 0; i < len(text); i++ { + // escape initial apostrophe or period + if len(text) >= 1 && (text[0] == '\'' || text[0] == '.') { + out.WriteString("\\&") + } + + // directly copy normal characters + org := i + + for i < len(text) && !needsBackslash(text[i]) { + i++ + } + if i > org { + out.Write(text[org:i]) + } + + // escape a character + if i >= len(text) { + break + } + out.WriteByte('\\') + out.WriteByte(text[i]) + } +} diff --git a/vendor/github.com/fatih/color/LICENSE.md b/vendor/github.com/fatih/color/LICENSE.md new file mode 100644 index 0000000000..25fdaf639d --- /dev/null +++ b/vendor/github.com/fatih/color/LICENSE.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013 Fatih Arslan + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/fatih/color/README.md b/vendor/github.com/fatih/color/README.md new file mode 100644 index 0000000000..5152bf59bf --- /dev/null +++ b/vendor/github.com/fatih/color/README.md @@ -0,0 +1,178 @@ +# color [![](https://github.com/fatih/color/workflows/build/badge.svg)](https://github.com/fatih/color/actions) [![PkgGoDev](https://pkg.go.dev/badge/github.com/fatih/color)](https://pkg.go.dev/github.com/fatih/color) + +Color lets you use colorized outputs in terms of [ANSI Escape +Codes](http://en.wikipedia.org/wiki/ANSI_escape_code#Colors) in Go (Golang). It +has support for Windows too! The API can be used in several ways, pick one that +suits you. + +![Color](https://user-images.githubusercontent.com/438920/96832689-03b3e000-13f4-11eb-9803-46f4c4de3406.jpg) + + +## Install + +```bash +go get github.com/fatih/color +``` + +## Examples + +### Standard colors + +```go +// Print with default helper functions +color.Cyan("Prints text in cyan.") + +// A newline will be appended automatically +color.Blue("Prints %s in blue.", "text") + +// These are using the default foreground colors +color.Red("We have red") +color.Magenta("And many others ..") + +``` + +### Mix and reuse colors + +```go +// Create a new color object +c := color.New(color.FgCyan).Add(color.Underline) +c.Println("Prints cyan text with an underline.") + +// Or just add them to New() +d := color.New(color.FgCyan, color.Bold) +d.Printf("This prints bold cyan %s\n", "too!.") + +// Mix up foreground and background colors, create new mixes! +red := color.New(color.FgRed) + +boldRed := red.Add(color.Bold) +boldRed.Println("This will print text in bold red.") + +whiteBackground := red.Add(color.BgWhite) +whiteBackground.Println("Red text with white background.") +``` + +### Use your own output (io.Writer) + +```go +// Use your own io.Writer output +color.New(color.FgBlue).Fprintln(myWriter, "blue color!") + +blue := color.New(color.FgBlue) +blue.Fprint(writer, "This will print text in blue.") +``` + +### Custom print functions (PrintFunc) + +```go +// Create a custom print function for convenience +red := color.New(color.FgRed).PrintfFunc() +red("Warning") +red("Error: %s", err) + +// Mix up multiple attributes +notice := color.New(color.Bold, color.FgGreen).PrintlnFunc() +notice("Don't forget this...") +``` + +### Custom fprint functions (FprintFunc) + +```go +blue := color.New(color.FgBlue).FprintfFunc() +blue(myWriter, "important notice: %s", stars) + +// Mix up with multiple attributes +success := color.New(color.Bold, color.FgGreen).FprintlnFunc() +success(myWriter, "Don't forget this...") +``` + +### Insert into noncolor strings (SprintFunc) + +```go +// Create SprintXxx functions to mix strings with other non-colorized strings: +yellow := color.New(color.FgYellow).SprintFunc() +red := color.New(color.FgRed).SprintFunc() +fmt.Printf("This is a %s and this is %s.\n", yellow("warning"), red("error")) + +info := color.New(color.FgWhite, color.BgGreen).SprintFunc() +fmt.Printf("This %s rocks!\n", info("package")) + +// Use helper functions +fmt.Println("This", color.RedString("warning"), "should be not neglected.") +fmt.Printf("%v %v\n", color.GreenString("Info:"), "an important message.") + +// Windows supported too! Just don't forget to change the output to color.Output +fmt.Fprintf(color.Output, "Windows support: %s", color.GreenString("PASS")) +``` + +### Plug into existing code + +```go +// Use handy standard colors +color.Set(color.FgYellow) + +fmt.Println("Existing text will now be in yellow") +fmt.Printf("This one %s\n", "too") + +color.Unset() // Don't forget to unset + +// You can mix up parameters +color.Set(color.FgMagenta, color.Bold) +defer color.Unset() // Use it in your function + +fmt.Println("All text will now be bold magenta.") +``` + +### Disable/Enable color + +There might be a case where you want to explicitly disable/enable color output. the +`go-isatty` package will automatically disable color output for non-tty output streams +(for example if the output were piped directly to `less`). + +The `color` package also disables color output if the [`NO_COLOR`](https://no-color.org) environment +variable is set (regardless of its value). + +`Color` has support to disable/enable colors programatically both globally and +for single color definitions. For example suppose you have a CLI app and a +`--no-color` bool flag. You can easily disable the color output with: + +```go +var flagNoColor = flag.Bool("no-color", false, "Disable color output") + +if *flagNoColor { + color.NoColor = true // disables colorized output +} +``` + +It also has support for single color definitions (local). You can +disable/enable color output on the fly: + +```go +c := color.New(color.FgCyan) +c.Println("Prints cyan text") + +c.DisableColor() +c.Println("This is printed without any color") + +c.EnableColor() +c.Println("This prints again cyan...") +``` + +## GitHub Actions + +To output color in GitHub Actions (or other CI systems that support ANSI colors), make sure to set `color.NoColor = false` so that it bypasses the check for non-tty output streams. + +## Todo + +* Save/Return previous values +* Evaluate fmt.Formatter interface + + +## Credits + + * [Fatih Arslan](https://github.com/fatih) + * Windows support via @mattn: [colorable](https://github.com/mattn/go-colorable) + +## License + +The MIT License (MIT) - see [`LICENSE.md`](https://github.com/fatih/color/blob/master/LICENSE.md) for more details diff --git a/vendor/github.com/fatih/color/color.go b/vendor/github.com/fatih/color/color.go new file mode 100644 index 0000000000..98a60f3c88 --- /dev/null +++ b/vendor/github.com/fatih/color/color.go @@ -0,0 +1,618 @@ +package color + +import ( + "fmt" + "io" + "os" + "strconv" + "strings" + "sync" + + "github.com/mattn/go-colorable" + "github.com/mattn/go-isatty" +) + +var ( + // NoColor defines if the output is colorized or not. It's dynamically set to + // false or true based on the stdout's file descriptor referring to a terminal + // or not. It's also set to true if the NO_COLOR environment variable is + // set (regardless of its value). This is a global option and affects all + // colors. For more control over each color block use the methods + // DisableColor() individually. + NoColor = noColorExists() || os.Getenv("TERM") == "dumb" || + (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) + + // Output defines the standard output of the print functions. By default + // os.Stdout is used. + Output = colorable.NewColorableStdout() + + // Error defines a color supporting writer for os.Stderr. + Error = colorable.NewColorableStderr() + + // colorsCache is used to reduce the count of created Color objects and + // allows to reuse already created objects with required Attribute. + colorsCache = make(map[Attribute]*Color) + colorsCacheMu sync.Mutex // protects colorsCache +) + +// noColorExists returns true if the environment variable NO_COLOR exists. +func noColorExists() bool { + _, exists := os.LookupEnv("NO_COLOR") + return exists +} + +// Color defines a custom color object which is defined by SGR parameters. +type Color struct { + params []Attribute + noColor *bool +} + +// Attribute defines a single SGR Code +type Attribute int + +const escape = "\x1b" + +// Base attributes +const ( + Reset Attribute = iota + Bold + Faint + Italic + Underline + BlinkSlow + BlinkRapid + ReverseVideo + Concealed + CrossedOut +) + +// Foreground text colors +const ( + FgBlack Attribute = iota + 30 + FgRed + FgGreen + FgYellow + FgBlue + FgMagenta + FgCyan + FgWhite +) + +// Foreground Hi-Intensity text colors +const ( + FgHiBlack Attribute = iota + 90 + FgHiRed + FgHiGreen + FgHiYellow + FgHiBlue + FgHiMagenta + FgHiCyan + FgHiWhite +) + +// Background text colors +const ( + BgBlack Attribute = iota + 40 + BgRed + BgGreen + BgYellow + BgBlue + BgMagenta + BgCyan + BgWhite +) + +// Background Hi-Intensity text colors +const ( + BgHiBlack Attribute = iota + 100 + BgHiRed + BgHiGreen + BgHiYellow + BgHiBlue + BgHiMagenta + BgHiCyan + BgHiWhite +) + +// New returns a newly created color object. +func New(value ...Attribute) *Color { + c := &Color{ + params: make([]Attribute, 0), + } + + if noColorExists() { + c.noColor = boolPtr(true) + } + + c.Add(value...) + return c +} + +// Set sets the given parameters immediately. It will change the color of +// output with the given SGR parameters until color.Unset() is called. +func Set(p ...Attribute) *Color { + c := New(p...) + c.Set() + return c +} + +// Unset resets all escape attributes and clears the output. Usually should +// be called after Set(). +func Unset() { + if NoColor { + return + } + + fmt.Fprintf(Output, "%s[%dm", escape, Reset) +} + +// Set sets the SGR sequence. +func (c *Color) Set() *Color { + if c.isNoColorSet() { + return c + } + + fmt.Fprintf(Output, c.format()) + return c +} + +func (c *Color) unset() { + if c.isNoColorSet() { + return + } + + Unset() +} + +func (c *Color) setWriter(w io.Writer) *Color { + if c.isNoColorSet() { + return c + } + + fmt.Fprintf(w, c.format()) + return c +} + +func (c *Color) unsetWriter(w io.Writer) { + if c.isNoColorSet() { + return + } + + if NoColor { + return + } + + fmt.Fprintf(w, "%s[%dm", escape, Reset) +} + +// Add is used to chain SGR parameters. Use as many as parameters to combine +// and create custom color objects. Example: Add(color.FgRed, color.Underline). +func (c *Color) Add(value ...Attribute) *Color { + c.params = append(c.params, value...) + return c +} + +func (c *Color) prepend(value Attribute) { + c.params = append(c.params, 0) + copy(c.params[1:], c.params[0:]) + c.params[0] = value +} + +// Fprint formats using the default formats for its operands and writes to w. +// Spaces are added between operands when neither is a string. +// It returns the number of bytes written and any write error encountered. +// On Windows, users should wrap w with colorable.NewColorable() if w is of +// type *os.File. +func (c *Color) Fprint(w io.Writer, a ...interface{}) (n int, err error) { + c.setWriter(w) + defer c.unsetWriter(w) + + return fmt.Fprint(w, a...) +} + +// Print formats using the default formats for its operands and writes to +// standard output. Spaces are added between operands when neither is a +// string. It returns the number of bytes written and any write error +// encountered. This is the standard fmt.Print() method wrapped with the given +// color. +func (c *Color) Print(a ...interface{}) (n int, err error) { + c.Set() + defer c.unset() + + return fmt.Fprint(Output, a...) +} + +// Fprintf formats according to a format specifier and writes to w. +// It returns the number of bytes written and any write error encountered. +// On Windows, users should wrap w with colorable.NewColorable() if w is of +// type *os.File. +func (c *Color) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { + c.setWriter(w) + defer c.unsetWriter(w) + + return fmt.Fprintf(w, format, a...) +} + +// Printf formats according to a format specifier and writes to standard output. +// It returns the number of bytes written and any write error encountered. +// This is the standard fmt.Printf() method wrapped with the given color. +func (c *Color) Printf(format string, a ...interface{}) (n int, err error) { + c.Set() + defer c.unset() + + return fmt.Fprintf(Output, format, a...) +} + +// Fprintln formats using the default formats for its operands and writes to w. +// Spaces are always added between operands and a newline is appended. +// On Windows, users should wrap w with colorable.NewColorable() if w is of +// type *os.File. +func (c *Color) Fprintln(w io.Writer, a ...interface{}) (n int, err error) { + c.setWriter(w) + defer c.unsetWriter(w) + + return fmt.Fprintln(w, a...) +} + +// Println formats using the default formats for its operands and writes to +// standard output. Spaces are always added between operands and a newline is +// appended. It returns the number of bytes written and any write error +// encountered. This is the standard fmt.Print() method wrapped with the given +// color. +func (c *Color) Println(a ...interface{}) (n int, err error) { + c.Set() + defer c.unset() + + return fmt.Fprintln(Output, a...) +} + +// Sprint is just like Print, but returns a string instead of printing it. +func (c *Color) Sprint(a ...interface{}) string { + return c.wrap(fmt.Sprint(a...)) +} + +// Sprintln is just like Println, but returns a string instead of printing it. +func (c *Color) Sprintln(a ...interface{}) string { + return c.wrap(fmt.Sprintln(a...)) +} + +// Sprintf is just like Printf, but returns a string instead of printing it. +func (c *Color) Sprintf(format string, a ...interface{}) string { + return c.wrap(fmt.Sprintf(format, a...)) +} + +// FprintFunc returns a new function that prints the passed arguments as +// colorized with color.Fprint(). +func (c *Color) FprintFunc() func(w io.Writer, a ...interface{}) { + return func(w io.Writer, a ...interface{}) { + c.Fprint(w, a...) + } +} + +// PrintFunc returns a new function that prints the passed arguments as +// colorized with color.Print(). +func (c *Color) PrintFunc() func(a ...interface{}) { + return func(a ...interface{}) { + c.Print(a...) + } +} + +// FprintfFunc returns a new function that prints the passed arguments as +// colorized with color.Fprintf(). +func (c *Color) FprintfFunc() func(w io.Writer, format string, a ...interface{}) { + return func(w io.Writer, format string, a ...interface{}) { + c.Fprintf(w, format, a...) + } +} + +// PrintfFunc returns a new function that prints the passed arguments as +// colorized with color.Printf(). +func (c *Color) PrintfFunc() func(format string, a ...interface{}) { + return func(format string, a ...interface{}) { + c.Printf(format, a...) + } +} + +// FprintlnFunc returns a new function that prints the passed arguments as +// colorized with color.Fprintln(). +func (c *Color) FprintlnFunc() func(w io.Writer, a ...interface{}) { + return func(w io.Writer, a ...interface{}) { + c.Fprintln(w, a...) + } +} + +// PrintlnFunc returns a new function that prints the passed arguments as +// colorized with color.Println(). +func (c *Color) PrintlnFunc() func(a ...interface{}) { + return func(a ...interface{}) { + c.Println(a...) + } +} + +// SprintFunc returns a new function that returns colorized strings for the +// given arguments with fmt.Sprint(). Useful to put into or mix into other +// string. Windows users should use this in conjunction with color.Output, example: +// +// put := New(FgYellow).SprintFunc() +// fmt.Fprintf(color.Output, "This is a %s", put("warning")) +func (c *Color) SprintFunc() func(a ...interface{}) string { + return func(a ...interface{}) string { + return c.wrap(fmt.Sprint(a...)) + } +} + +// SprintfFunc returns a new function that returns colorized strings for the +// given arguments with fmt.Sprintf(). Useful to put into or mix into other +// string. Windows users should use this in conjunction with color.Output. +func (c *Color) SprintfFunc() func(format string, a ...interface{}) string { + return func(format string, a ...interface{}) string { + return c.wrap(fmt.Sprintf(format, a...)) + } +} + +// SprintlnFunc returns a new function that returns colorized strings for the +// given arguments with fmt.Sprintln(). Useful to put into or mix into other +// string. Windows users should use this in conjunction with color.Output. +func (c *Color) SprintlnFunc() func(a ...interface{}) string { + return func(a ...interface{}) string { + return c.wrap(fmt.Sprintln(a...)) + } +} + +// sequence returns a formatted SGR sequence to be plugged into a "\x1b[...m" +// an example output might be: "1;36" -> bold cyan +func (c *Color) sequence() string { + format := make([]string, len(c.params)) + for i, v := range c.params { + format[i] = strconv.Itoa(int(v)) + } + + return strings.Join(format, ";") +} + +// wrap wraps the s string with the colors attributes. The string is ready to +// be printed. +func (c *Color) wrap(s string) string { + if c.isNoColorSet() { + return s + } + + return c.format() + s + c.unformat() +} + +func (c *Color) format() string { + return fmt.Sprintf("%s[%sm", escape, c.sequence()) +} + +func (c *Color) unformat() string { + return fmt.Sprintf("%s[%dm", escape, Reset) +} + +// DisableColor disables the color output. Useful to not change any existing +// code and still being able to output. Can be used for flags like +// "--no-color". To enable back use EnableColor() method. +func (c *Color) DisableColor() { + c.noColor = boolPtr(true) +} + +// EnableColor enables the color output. Use it in conjunction with +// DisableColor(). Otherwise this method has no side effects. +func (c *Color) EnableColor() { + c.noColor = boolPtr(false) +} + +func (c *Color) isNoColorSet() bool { + // check first if we have user set action + if c.noColor != nil { + return *c.noColor + } + + // if not return the global option, which is disabled by default + return NoColor +} + +// Equals returns a boolean value indicating whether two colors are equal. +func (c *Color) Equals(c2 *Color) bool { + if len(c.params) != len(c2.params) { + return false + } + + for _, attr := range c.params { + if !c2.attrExists(attr) { + return false + } + } + + return true +} + +func (c *Color) attrExists(a Attribute) bool { + for _, attr := range c.params { + if attr == a { + return true + } + } + + return false +} + +func boolPtr(v bool) *bool { + return &v +} + +func getCachedColor(p Attribute) *Color { + colorsCacheMu.Lock() + defer colorsCacheMu.Unlock() + + c, ok := colorsCache[p] + if !ok { + c = New(p) + colorsCache[p] = c + } + + return c +} + +func colorPrint(format string, p Attribute, a ...interface{}) { + c := getCachedColor(p) + + if !strings.HasSuffix(format, "\n") { + format += "\n" + } + + if len(a) == 0 { + c.Print(format) + } else { + c.Printf(format, a...) + } +} + +func colorString(format string, p Attribute, a ...interface{}) string { + c := getCachedColor(p) + + if len(a) == 0 { + return c.SprintFunc()(format) + } + + return c.SprintfFunc()(format, a...) +} + +// Black is a convenient helper function to print with black foreground. A +// newline is appended to format by default. +func Black(format string, a ...interface{}) { colorPrint(format, FgBlack, a...) } + +// Red is a convenient helper function to print with red foreground. A +// newline is appended to format by default. +func Red(format string, a ...interface{}) { colorPrint(format, FgRed, a...) } + +// Green is a convenient helper function to print with green foreground. A +// newline is appended to format by default. +func Green(format string, a ...interface{}) { colorPrint(format, FgGreen, a...) } + +// Yellow is a convenient helper function to print with yellow foreground. +// A newline is appended to format by default. +func Yellow(format string, a ...interface{}) { colorPrint(format, FgYellow, a...) } + +// Blue is a convenient helper function to print with blue foreground. A +// newline is appended to format by default. +func Blue(format string, a ...interface{}) { colorPrint(format, FgBlue, a...) } + +// Magenta is a convenient helper function to print with magenta foreground. +// A newline is appended to format by default. +func Magenta(format string, a ...interface{}) { colorPrint(format, FgMagenta, a...) } + +// Cyan is a convenient helper function to print with cyan foreground. A +// newline is appended to format by default. +func Cyan(format string, a ...interface{}) { colorPrint(format, FgCyan, a...) } + +// White is a convenient helper function to print with white foreground. A +// newline is appended to format by default. +func White(format string, a ...interface{}) { colorPrint(format, FgWhite, a...) } + +// BlackString is a convenient helper function to return a string with black +// foreground. +func BlackString(format string, a ...interface{}) string { return colorString(format, FgBlack, a...) } + +// RedString is a convenient helper function to return a string with red +// foreground. +func RedString(format string, a ...interface{}) string { return colorString(format, FgRed, a...) } + +// GreenString is a convenient helper function to return a string with green +// foreground. +func GreenString(format string, a ...interface{}) string { return colorString(format, FgGreen, a...) } + +// YellowString is a convenient helper function to return a string with yellow +// foreground. +func YellowString(format string, a ...interface{}) string { return colorString(format, FgYellow, a...) } + +// BlueString is a convenient helper function to return a string with blue +// foreground. +func BlueString(format string, a ...interface{}) string { return colorString(format, FgBlue, a...) } + +// MagentaString is a convenient helper function to return a string with magenta +// foreground. +func MagentaString(format string, a ...interface{}) string { + return colorString(format, FgMagenta, a...) +} + +// CyanString is a convenient helper function to return a string with cyan +// foreground. +func CyanString(format string, a ...interface{}) string { return colorString(format, FgCyan, a...) } + +// WhiteString is a convenient helper function to return a string with white +// foreground. +func WhiteString(format string, a ...interface{}) string { return colorString(format, FgWhite, a...) } + +// HiBlack is a convenient helper function to print with hi-intensity black foreground. A +// newline is appended to format by default. +func HiBlack(format string, a ...interface{}) { colorPrint(format, FgHiBlack, a...) } + +// HiRed is a convenient helper function to print with hi-intensity red foreground. A +// newline is appended to format by default. +func HiRed(format string, a ...interface{}) { colorPrint(format, FgHiRed, a...) } + +// HiGreen is a convenient helper function to print with hi-intensity green foreground. A +// newline is appended to format by default. +func HiGreen(format string, a ...interface{}) { colorPrint(format, FgHiGreen, a...) } + +// HiYellow is a convenient helper function to print with hi-intensity yellow foreground. +// A newline is appended to format by default. +func HiYellow(format string, a ...interface{}) { colorPrint(format, FgHiYellow, a...) } + +// HiBlue is a convenient helper function to print with hi-intensity blue foreground. A +// newline is appended to format by default. +func HiBlue(format string, a ...interface{}) { colorPrint(format, FgHiBlue, a...) } + +// HiMagenta is a convenient helper function to print with hi-intensity magenta foreground. +// A newline is appended to format by default. +func HiMagenta(format string, a ...interface{}) { colorPrint(format, FgHiMagenta, a...) } + +// HiCyan is a convenient helper function to print with hi-intensity cyan foreground. A +// newline is appended to format by default. +func HiCyan(format string, a ...interface{}) { colorPrint(format, FgHiCyan, a...) } + +// HiWhite is a convenient helper function to print with hi-intensity white foreground. A +// newline is appended to format by default. +func HiWhite(format string, a ...interface{}) { colorPrint(format, FgHiWhite, a...) } + +// HiBlackString is a convenient helper function to return a string with hi-intensity black +// foreground. +func HiBlackString(format string, a ...interface{}) string { + return colorString(format, FgHiBlack, a...) +} + +// HiRedString is a convenient helper function to return a string with hi-intensity red +// foreground. +func HiRedString(format string, a ...interface{}) string { return colorString(format, FgHiRed, a...) } + +// HiGreenString is a convenient helper function to return a string with hi-intensity green +// foreground. +func HiGreenString(format string, a ...interface{}) string { + return colorString(format, FgHiGreen, a...) +} + +// HiYellowString is a convenient helper function to return a string with hi-intensity yellow +// foreground. +func HiYellowString(format string, a ...interface{}) string { + return colorString(format, FgHiYellow, a...) +} + +// HiBlueString is a convenient helper function to return a string with hi-intensity blue +// foreground. +func HiBlueString(format string, a ...interface{}) string { return colorString(format, FgHiBlue, a...) } + +// HiMagentaString is a convenient helper function to return a string with hi-intensity magenta +// foreground. +func HiMagentaString(format string, a ...interface{}) string { + return colorString(format, FgHiMagenta, a...) +} + +// HiCyanString is a convenient helper function to return a string with hi-intensity cyan +// foreground. +func HiCyanString(format string, a ...interface{}) string { return colorString(format, FgHiCyan, a...) } + +// HiWhiteString is a convenient helper function to return a string with hi-intensity white +// foreground. +func HiWhiteString(format string, a ...interface{}) string { + return colorString(format, FgHiWhite, a...) +} diff --git a/vendor/github.com/fatih/color/doc.go b/vendor/github.com/fatih/color/doc.go new file mode 100644 index 0000000000..04541de786 --- /dev/null +++ b/vendor/github.com/fatih/color/doc.go @@ -0,0 +1,135 @@ +/* +Package color is an ANSI color package to output colorized or SGR defined +output to the standard output. The API can be used in several way, pick one +that suits you. + +Use simple and default helper functions with predefined foreground colors: + + color.Cyan("Prints text in cyan.") + + // a newline will be appended automatically + color.Blue("Prints %s in blue.", "text") + + // More default foreground colors.. + color.Red("We have red") + color.Yellow("Yellow color too!") + color.Magenta("And many others ..") + + // Hi-intensity colors + color.HiGreen("Bright green color.") + color.HiBlack("Bright black means gray..") + color.HiWhite("Shiny white color!") + +However there are times where custom color mixes are required. Below are some +examples to create custom color objects and use the print functions of each +separate color object. + + // Create a new color object + c := color.New(color.FgCyan).Add(color.Underline) + c.Println("Prints cyan text with an underline.") + + // Or just add them to New() + d := color.New(color.FgCyan, color.Bold) + d.Printf("This prints bold cyan %s\n", "too!.") + + + // Mix up foreground and background colors, create new mixes! + red := color.New(color.FgRed) + + boldRed := red.Add(color.Bold) + boldRed.Println("This will print text in bold red.") + + whiteBackground := red.Add(color.BgWhite) + whiteBackground.Println("Red text with White background.") + + // Use your own io.Writer output + color.New(color.FgBlue).Fprintln(myWriter, "blue color!") + + blue := color.New(color.FgBlue) + blue.Fprint(myWriter, "This will print text in blue.") + +You can create PrintXxx functions to simplify even more: + + // Create a custom print function for convenient + red := color.New(color.FgRed).PrintfFunc() + red("warning") + red("error: %s", err) + + // Mix up multiple attributes + notice := color.New(color.Bold, color.FgGreen).PrintlnFunc() + notice("don't forget this...") + +You can also FprintXxx functions to pass your own io.Writer: + + blue := color.New(FgBlue).FprintfFunc() + blue(myWriter, "important notice: %s", stars) + + // Mix up with multiple attributes + success := color.New(color.Bold, color.FgGreen).FprintlnFunc() + success(myWriter, don't forget this...") + + +Or create SprintXxx functions to mix strings with other non-colorized strings: + + yellow := New(FgYellow).SprintFunc() + red := New(FgRed).SprintFunc() + + fmt.Printf("this is a %s and this is %s.\n", yellow("warning"), red("error")) + + info := New(FgWhite, BgGreen).SprintFunc() + fmt.Printf("this %s rocks!\n", info("package")) + +Windows support is enabled by default. All Print functions work as intended. +However only for color.SprintXXX functions, user should use fmt.FprintXXX and +set the output to color.Output: + + fmt.Fprintf(color.Output, "Windows support: %s", color.GreenString("PASS")) + + info := New(FgWhite, BgGreen).SprintFunc() + fmt.Fprintf(color.Output, "this %s rocks!\n", info("package")) + +Using with existing code is possible. Just use the Set() method to set the +standard output to the given parameters. That way a rewrite of an existing +code is not required. + + // Use handy standard colors. + color.Set(color.FgYellow) + + fmt.Println("Existing text will be now in Yellow") + fmt.Printf("This one %s\n", "too") + + color.Unset() // don't forget to unset + + // You can mix up parameters + color.Set(color.FgMagenta, color.Bold) + defer color.Unset() // use it in your function + + fmt.Println("All text will be now bold magenta.") + +There might be a case where you want to disable color output (for example to +pipe the standard output of your app to somewhere else). `Color` has support to +disable colors both globally and for single color definition. For example +suppose you have a CLI app and a `--no-color` bool flag. You can easily disable +the color output with: + + var flagNoColor = flag.Bool("no-color", false, "Disable color output") + + if *flagNoColor { + color.NoColor = true // disables colorized output + } + +You can also disable the color by setting the NO_COLOR environment variable to any value. + +It also has support for single color definitions (local). You can +disable/enable color output on the fly: + + c := color.New(color.FgCyan) + c.Println("Prints cyan text") + + c.DisableColor() + c.Println("This is printed without any color") + + c.EnableColor() + c.Println("This prints again cyan...") +*/ +package color diff --git a/vendor/github.com/konsorten/go-windows-terminal-sequences/LICENSE b/vendor/github.com/konsorten/go-windows-terminal-sequences/LICENSE new file mode 100644 index 0000000000..14127cd831 --- /dev/null +++ b/vendor/github.com/konsorten/go-windows-terminal-sequences/LICENSE @@ -0,0 +1,9 @@ +(The MIT License) + +Copyright (c) 2017 marvin + konsorten GmbH (open-source@konsorten.de) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/konsorten/go-windows-terminal-sequences/README.md b/vendor/github.com/konsorten/go-windows-terminal-sequences/README.md new file mode 100644 index 0000000000..949b77e304 --- /dev/null +++ b/vendor/github.com/konsorten/go-windows-terminal-sequences/README.md @@ -0,0 +1,40 @@ +# Windows Terminal Sequences + +This library allow for enabling Windows terminal color support for Go. + +See [Console Virtual Terminal Sequences](https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences) for details. + +## Usage + +```go +import ( + "syscall" + + sequences "github.com/konsorten/go-windows-terminal-sequences" +) + +func main() { + sequences.EnableVirtualTerminalProcessing(syscall.Stdout, true) +} + +``` + +## Authors + +The tool is sponsored by the [marvin + konsorten GmbH](http://www.konsorten.de). + +We thank all the authors who provided code to this library: + +* Felix Kollmann + +## License + +(The MIT License) + +Copyright (c) 2018 marvin + konsorten GmbH (open-source@konsorten.de) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/konsorten/go-windows-terminal-sequences/sequences.go b/vendor/github.com/konsorten/go-windows-terminal-sequences/sequences.go new file mode 100644 index 0000000000..ef18d8f978 --- /dev/null +++ b/vendor/github.com/konsorten/go-windows-terminal-sequences/sequences.go @@ -0,0 +1,36 @@ +// +build windows + +package sequences + +import ( + "syscall" + "unsafe" +) + +var ( + kernel32Dll *syscall.LazyDLL = syscall.NewLazyDLL("Kernel32.dll") + setConsoleMode *syscall.LazyProc = kernel32Dll.NewProc("SetConsoleMode") +) + +func EnableVirtualTerminalProcessing(stream syscall.Handle, enable bool) error { + const ENABLE_VIRTUAL_TERMINAL_PROCESSING uint32 = 0x4 + + var mode uint32 + err := syscall.GetConsoleMode(syscall.Stdout, &mode) + if err != nil { + return err + } + + if enable { + mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING + } else { + mode &^= ENABLE_VIRTUAL_TERMINAL_PROCESSING + } + + ret, _, err := setConsoleMode.Call(uintptr(unsafe.Pointer(stream)), uintptr(mode)) + if ret == 0 { + return err + } + + return nil +} diff --git a/vendor/github.com/mattn/go-colorable/.travis.yml b/vendor/github.com/mattn/go-colorable/.travis.yml new file mode 100644 index 0000000000..7942c565ce --- /dev/null +++ b/vendor/github.com/mattn/go-colorable/.travis.yml @@ -0,0 +1,15 @@ +language: go +sudo: false +go: + - 1.13.x + - tip + +before_install: + - go get -t -v ./... + +script: + - ./go.test.sh + +after_success: + - bash <(curl -s https://codecov.io/bash) + diff --git a/vendor/github.com/mattn/go-colorable/LICENSE b/vendor/github.com/mattn/go-colorable/LICENSE new file mode 100644 index 0000000000..91b5cef30e --- /dev/null +++ b/vendor/github.com/mattn/go-colorable/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Yasuhiro Matsumoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/mattn/go-colorable/README.md b/vendor/github.com/mattn/go-colorable/README.md new file mode 100644 index 0000000000..e055952b66 --- /dev/null +++ b/vendor/github.com/mattn/go-colorable/README.md @@ -0,0 +1,48 @@ +# go-colorable + +[![Build Status](https://travis-ci.org/mattn/go-colorable.svg?branch=master)](https://travis-ci.org/mattn/go-colorable) +[![Codecov](https://codecov.io/gh/mattn/go-colorable/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-colorable) +[![GoDoc](https://godoc.org/github.com/mattn/go-colorable?status.svg)](http://godoc.org/github.com/mattn/go-colorable) +[![Go Report Card](https://goreportcard.com/badge/mattn/go-colorable)](https://goreportcard.com/report/mattn/go-colorable) + +Colorable writer for windows. + +For example, most of logger packages doesn't show colors on windows. (I know we can do it with ansicon. But I don't want.) +This package is possible to handle escape sequence for ansi color on windows. + +## Too Bad! + +![](https://raw.githubusercontent.com/mattn/go-colorable/gh-pages/bad.png) + + +## So Good! + +![](https://raw.githubusercontent.com/mattn/go-colorable/gh-pages/good.png) + +## Usage + +```go +logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true}) +logrus.SetOutput(colorable.NewColorableStdout()) + +logrus.Info("succeeded") +logrus.Warn("not correct") +logrus.Error("something error") +logrus.Fatal("panic") +``` + +You can compile above code on non-windows OSs. + +## Installation + +``` +$ go get github.com/mattn/go-colorable +``` + +# License + +MIT + +# Author + +Yasuhiro Matsumoto (a.k.a mattn) diff --git a/vendor/github.com/mattn/go-colorable/colorable_appengine.go b/vendor/github.com/mattn/go-colorable/colorable_appengine.go new file mode 100644 index 0000000000..1f7806fe16 --- /dev/null +++ b/vendor/github.com/mattn/go-colorable/colorable_appengine.go @@ -0,0 +1,37 @@ +// +build appengine + +package colorable + +import ( + "io" + "os" + + _ "github.com/mattn/go-isatty" +) + +// NewColorable returns new instance of Writer which handles escape sequence. +func NewColorable(file *os.File) io.Writer { + if file == nil { + panic("nil passed instead of *os.File to NewColorable()") + } + + return file +} + +// NewColorableStdout returns new instance of Writer which handles escape sequence for stdout. +func NewColorableStdout() io.Writer { + return os.Stdout +} + +// NewColorableStderr returns new instance of Writer which handles escape sequence for stderr. +func NewColorableStderr() io.Writer { + return os.Stderr +} + +// EnableColorsStdout enable colors if possible. +func EnableColorsStdout(enabled *bool) func() { + if enabled != nil { + *enabled = true + } + return func() {} +} diff --git a/vendor/github.com/mattn/go-colorable/colorable_others.go b/vendor/github.com/mattn/go-colorable/colorable_others.go new file mode 100644 index 0000000000..08cbd1e0fa --- /dev/null +++ b/vendor/github.com/mattn/go-colorable/colorable_others.go @@ -0,0 +1,38 @@ +// +build !windows +// +build !appengine + +package colorable + +import ( + "io" + "os" + + _ "github.com/mattn/go-isatty" +) + +// NewColorable returns new instance of Writer which handles escape sequence. +func NewColorable(file *os.File) io.Writer { + if file == nil { + panic("nil passed instead of *os.File to NewColorable()") + } + + return file +} + +// NewColorableStdout returns new instance of Writer which handles escape sequence for stdout. +func NewColorableStdout() io.Writer { + return os.Stdout +} + +// NewColorableStderr returns new instance of Writer which handles escape sequence for stderr. +func NewColorableStderr() io.Writer { + return os.Stderr +} + +// EnableColorsStdout enable colors if possible. +func EnableColorsStdout(enabled *bool) func() { + if enabled != nil { + *enabled = true + } + return func() {} +} diff --git a/vendor/github.com/mattn/go-colorable/colorable_windows.go b/vendor/github.com/mattn/go-colorable/colorable_windows.go new file mode 100644 index 0000000000..41215d7fc4 --- /dev/null +++ b/vendor/github.com/mattn/go-colorable/colorable_windows.go @@ -0,0 +1,1043 @@ +// +build windows +// +build !appengine + +package colorable + +import ( + "bytes" + "io" + "math" + "os" + "strconv" + "strings" + "sync" + "syscall" + "unsafe" + + "github.com/mattn/go-isatty" +) + +const ( + foregroundBlue = 0x1 + foregroundGreen = 0x2 + foregroundRed = 0x4 + foregroundIntensity = 0x8 + foregroundMask = (foregroundRed | foregroundBlue | foregroundGreen | foregroundIntensity) + backgroundBlue = 0x10 + backgroundGreen = 0x20 + backgroundRed = 0x40 + backgroundIntensity = 0x80 + backgroundMask = (backgroundRed | backgroundBlue | backgroundGreen | backgroundIntensity) + commonLvbUnderscore = 0x8000 + + cENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x4 +) + +const ( + genericRead = 0x80000000 + genericWrite = 0x40000000 +) + +const ( + consoleTextmodeBuffer = 0x1 +) + +type wchar uint16 +type short int16 +type dword uint32 +type word uint16 + +type coord struct { + x short + y short +} + +type smallRect struct { + left short + top short + right short + bottom short +} + +type consoleScreenBufferInfo struct { + size coord + cursorPosition coord + attributes word + window smallRect + maximumWindowSize coord +} + +type consoleCursorInfo struct { + size dword + visible int32 +} + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") + procSetConsoleTextAttribute = kernel32.NewProc("SetConsoleTextAttribute") + procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition") + procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW") + procFillConsoleOutputAttribute = kernel32.NewProc("FillConsoleOutputAttribute") + procGetConsoleCursorInfo = kernel32.NewProc("GetConsoleCursorInfo") + procSetConsoleCursorInfo = kernel32.NewProc("SetConsoleCursorInfo") + procSetConsoleTitle = kernel32.NewProc("SetConsoleTitleW") + procGetConsoleMode = kernel32.NewProc("GetConsoleMode") + procSetConsoleMode = kernel32.NewProc("SetConsoleMode") + procCreateConsoleScreenBuffer = kernel32.NewProc("CreateConsoleScreenBuffer") +) + +// Writer provides colorable Writer to the console +type Writer struct { + out io.Writer + handle syscall.Handle + althandle syscall.Handle + oldattr word + oldpos coord + rest bytes.Buffer + mutex sync.Mutex +} + +// NewColorable returns new instance of Writer which handles escape sequence from File. +func NewColorable(file *os.File) io.Writer { + if file == nil { + panic("nil passed instead of *os.File to NewColorable()") + } + + if isatty.IsTerminal(file.Fd()) { + var mode uint32 + if r, _, _ := procGetConsoleMode.Call(file.Fd(), uintptr(unsafe.Pointer(&mode))); r != 0 && mode&cENABLE_VIRTUAL_TERMINAL_PROCESSING != 0 { + return file + } + var csbi consoleScreenBufferInfo + handle := syscall.Handle(file.Fd()) + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + return &Writer{out: file, handle: handle, oldattr: csbi.attributes, oldpos: coord{0, 0}} + } + return file +} + +// NewColorableStdout returns new instance of Writer which handles escape sequence for stdout. +func NewColorableStdout() io.Writer { + return NewColorable(os.Stdout) +} + +// NewColorableStderr returns new instance of Writer which handles escape sequence for stderr. +func NewColorableStderr() io.Writer { + return NewColorable(os.Stderr) +} + +var color256 = map[int]int{ + 0: 0x000000, + 1: 0x800000, + 2: 0x008000, + 3: 0x808000, + 4: 0x000080, + 5: 0x800080, + 6: 0x008080, + 7: 0xc0c0c0, + 8: 0x808080, + 9: 0xff0000, + 10: 0x00ff00, + 11: 0xffff00, + 12: 0x0000ff, + 13: 0xff00ff, + 14: 0x00ffff, + 15: 0xffffff, + 16: 0x000000, + 17: 0x00005f, + 18: 0x000087, + 19: 0x0000af, + 20: 0x0000d7, + 21: 0x0000ff, + 22: 0x005f00, + 23: 0x005f5f, + 24: 0x005f87, + 25: 0x005faf, + 26: 0x005fd7, + 27: 0x005fff, + 28: 0x008700, + 29: 0x00875f, + 30: 0x008787, + 31: 0x0087af, + 32: 0x0087d7, + 33: 0x0087ff, + 34: 0x00af00, + 35: 0x00af5f, + 36: 0x00af87, + 37: 0x00afaf, + 38: 0x00afd7, + 39: 0x00afff, + 40: 0x00d700, + 41: 0x00d75f, + 42: 0x00d787, + 43: 0x00d7af, + 44: 0x00d7d7, + 45: 0x00d7ff, + 46: 0x00ff00, + 47: 0x00ff5f, + 48: 0x00ff87, + 49: 0x00ffaf, + 50: 0x00ffd7, + 51: 0x00ffff, + 52: 0x5f0000, + 53: 0x5f005f, + 54: 0x5f0087, + 55: 0x5f00af, + 56: 0x5f00d7, + 57: 0x5f00ff, + 58: 0x5f5f00, + 59: 0x5f5f5f, + 60: 0x5f5f87, + 61: 0x5f5faf, + 62: 0x5f5fd7, + 63: 0x5f5fff, + 64: 0x5f8700, + 65: 0x5f875f, + 66: 0x5f8787, + 67: 0x5f87af, + 68: 0x5f87d7, + 69: 0x5f87ff, + 70: 0x5faf00, + 71: 0x5faf5f, + 72: 0x5faf87, + 73: 0x5fafaf, + 74: 0x5fafd7, + 75: 0x5fafff, + 76: 0x5fd700, + 77: 0x5fd75f, + 78: 0x5fd787, + 79: 0x5fd7af, + 80: 0x5fd7d7, + 81: 0x5fd7ff, + 82: 0x5fff00, + 83: 0x5fff5f, + 84: 0x5fff87, + 85: 0x5fffaf, + 86: 0x5fffd7, + 87: 0x5fffff, + 88: 0x870000, + 89: 0x87005f, + 90: 0x870087, + 91: 0x8700af, + 92: 0x8700d7, + 93: 0x8700ff, + 94: 0x875f00, + 95: 0x875f5f, + 96: 0x875f87, + 97: 0x875faf, + 98: 0x875fd7, + 99: 0x875fff, + 100: 0x878700, + 101: 0x87875f, + 102: 0x878787, + 103: 0x8787af, + 104: 0x8787d7, + 105: 0x8787ff, + 106: 0x87af00, + 107: 0x87af5f, + 108: 0x87af87, + 109: 0x87afaf, + 110: 0x87afd7, + 111: 0x87afff, + 112: 0x87d700, + 113: 0x87d75f, + 114: 0x87d787, + 115: 0x87d7af, + 116: 0x87d7d7, + 117: 0x87d7ff, + 118: 0x87ff00, + 119: 0x87ff5f, + 120: 0x87ff87, + 121: 0x87ffaf, + 122: 0x87ffd7, + 123: 0x87ffff, + 124: 0xaf0000, + 125: 0xaf005f, + 126: 0xaf0087, + 127: 0xaf00af, + 128: 0xaf00d7, + 129: 0xaf00ff, + 130: 0xaf5f00, + 131: 0xaf5f5f, + 132: 0xaf5f87, + 133: 0xaf5faf, + 134: 0xaf5fd7, + 135: 0xaf5fff, + 136: 0xaf8700, + 137: 0xaf875f, + 138: 0xaf8787, + 139: 0xaf87af, + 140: 0xaf87d7, + 141: 0xaf87ff, + 142: 0xafaf00, + 143: 0xafaf5f, + 144: 0xafaf87, + 145: 0xafafaf, + 146: 0xafafd7, + 147: 0xafafff, + 148: 0xafd700, + 149: 0xafd75f, + 150: 0xafd787, + 151: 0xafd7af, + 152: 0xafd7d7, + 153: 0xafd7ff, + 154: 0xafff00, + 155: 0xafff5f, + 156: 0xafff87, + 157: 0xafffaf, + 158: 0xafffd7, + 159: 0xafffff, + 160: 0xd70000, + 161: 0xd7005f, + 162: 0xd70087, + 163: 0xd700af, + 164: 0xd700d7, + 165: 0xd700ff, + 166: 0xd75f00, + 167: 0xd75f5f, + 168: 0xd75f87, + 169: 0xd75faf, + 170: 0xd75fd7, + 171: 0xd75fff, + 172: 0xd78700, + 173: 0xd7875f, + 174: 0xd78787, + 175: 0xd787af, + 176: 0xd787d7, + 177: 0xd787ff, + 178: 0xd7af00, + 179: 0xd7af5f, + 180: 0xd7af87, + 181: 0xd7afaf, + 182: 0xd7afd7, + 183: 0xd7afff, + 184: 0xd7d700, + 185: 0xd7d75f, + 186: 0xd7d787, + 187: 0xd7d7af, + 188: 0xd7d7d7, + 189: 0xd7d7ff, + 190: 0xd7ff00, + 191: 0xd7ff5f, + 192: 0xd7ff87, + 193: 0xd7ffaf, + 194: 0xd7ffd7, + 195: 0xd7ffff, + 196: 0xff0000, + 197: 0xff005f, + 198: 0xff0087, + 199: 0xff00af, + 200: 0xff00d7, + 201: 0xff00ff, + 202: 0xff5f00, + 203: 0xff5f5f, + 204: 0xff5f87, + 205: 0xff5faf, + 206: 0xff5fd7, + 207: 0xff5fff, + 208: 0xff8700, + 209: 0xff875f, + 210: 0xff8787, + 211: 0xff87af, + 212: 0xff87d7, + 213: 0xff87ff, + 214: 0xffaf00, + 215: 0xffaf5f, + 216: 0xffaf87, + 217: 0xffafaf, + 218: 0xffafd7, + 219: 0xffafff, + 220: 0xffd700, + 221: 0xffd75f, + 222: 0xffd787, + 223: 0xffd7af, + 224: 0xffd7d7, + 225: 0xffd7ff, + 226: 0xffff00, + 227: 0xffff5f, + 228: 0xffff87, + 229: 0xffffaf, + 230: 0xffffd7, + 231: 0xffffff, + 232: 0x080808, + 233: 0x121212, + 234: 0x1c1c1c, + 235: 0x262626, + 236: 0x303030, + 237: 0x3a3a3a, + 238: 0x444444, + 239: 0x4e4e4e, + 240: 0x585858, + 241: 0x626262, + 242: 0x6c6c6c, + 243: 0x767676, + 244: 0x808080, + 245: 0x8a8a8a, + 246: 0x949494, + 247: 0x9e9e9e, + 248: 0xa8a8a8, + 249: 0xb2b2b2, + 250: 0xbcbcbc, + 251: 0xc6c6c6, + 252: 0xd0d0d0, + 253: 0xdadada, + 254: 0xe4e4e4, + 255: 0xeeeeee, +} + +// `\033]0;TITLESTR\007` +func doTitleSequence(er *bytes.Reader) error { + var c byte + var err error + + c, err = er.ReadByte() + if err != nil { + return err + } + if c != '0' && c != '2' { + return nil + } + c, err = er.ReadByte() + if err != nil { + return err + } + if c != ';' { + return nil + } + title := make([]byte, 0, 80) + for { + c, err = er.ReadByte() + if err != nil { + return err + } + if c == 0x07 || c == '\n' { + break + } + title = append(title, c) + } + if len(title) > 0 { + title8, err := syscall.UTF16PtrFromString(string(title)) + if err == nil { + procSetConsoleTitle.Call(uintptr(unsafe.Pointer(title8))) + } + } + return nil +} + +// returns Atoi(s) unless s == "" in which case it returns def +func atoiWithDefault(s string, def int) (int, error) { + if s == "" { + return def, nil + } + return strconv.Atoi(s) +} + +// Write writes data on console +func (w *Writer) Write(data []byte) (n int, err error) { + w.mutex.Lock() + defer w.mutex.Unlock() + var csbi consoleScreenBufferInfo + procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) + + handle := w.handle + + var er *bytes.Reader + if w.rest.Len() > 0 { + var rest bytes.Buffer + w.rest.WriteTo(&rest) + w.rest.Reset() + rest.Write(data) + er = bytes.NewReader(rest.Bytes()) + } else { + er = bytes.NewReader(data) + } + var bw [1]byte +loop: + for { + c1, err := er.ReadByte() + if err != nil { + break loop + } + if c1 != 0x1b { + bw[0] = c1 + w.out.Write(bw[:]) + continue + } + c2, err := er.ReadByte() + if err != nil { + break loop + } + + switch c2 { + case '>': + continue + case ']': + w.rest.WriteByte(c1) + w.rest.WriteByte(c2) + er.WriteTo(&w.rest) + if bytes.IndexByte(w.rest.Bytes(), 0x07) == -1 { + break loop + } + er = bytes.NewReader(w.rest.Bytes()[2:]) + err := doTitleSequence(er) + if err != nil { + break loop + } + w.rest.Reset() + continue + // https://github.com/mattn/go-colorable/issues/27 + case '7': + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + w.oldpos = csbi.cursorPosition + continue + case '8': + procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&w.oldpos))) + continue + case 0x5b: + // execute part after switch + default: + continue + } + + w.rest.WriteByte(c1) + w.rest.WriteByte(c2) + er.WriteTo(&w.rest) + + var buf bytes.Buffer + var m byte + for i, c := range w.rest.Bytes()[2:] { + if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' { + m = c + er = bytes.NewReader(w.rest.Bytes()[2+i+1:]) + w.rest.Reset() + break + } + buf.Write([]byte(string(c))) + } + if m == 0 { + break loop + } + + switch m { + case 'A': + n, err = atoiWithDefault(buf.String(), 1) + if err != nil { + continue + } + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + csbi.cursorPosition.y -= short(n) + procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) + case 'B': + n, err = atoiWithDefault(buf.String(), 1) + if err != nil { + continue + } + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + csbi.cursorPosition.y += short(n) + procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) + case 'C': + n, err = atoiWithDefault(buf.String(), 1) + if err != nil { + continue + } + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + csbi.cursorPosition.x += short(n) + procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) + case 'D': + n, err = atoiWithDefault(buf.String(), 1) + if err != nil { + continue + } + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + csbi.cursorPosition.x -= short(n) + if csbi.cursorPosition.x < 0 { + csbi.cursorPosition.x = 0 + } + procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) + case 'E': + n, err = strconv.Atoi(buf.String()) + if err != nil { + continue + } + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + csbi.cursorPosition.x = 0 + csbi.cursorPosition.y += short(n) + procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) + case 'F': + n, err = strconv.Atoi(buf.String()) + if err != nil { + continue + } + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + csbi.cursorPosition.x = 0 + csbi.cursorPosition.y -= short(n) + procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) + case 'G': + n, err = strconv.Atoi(buf.String()) + if err != nil { + continue + } + if n < 1 { + n = 1 + } + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + csbi.cursorPosition.x = short(n - 1) + procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) + case 'H', 'f': + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + if buf.Len() > 0 { + token := strings.Split(buf.String(), ";") + switch len(token) { + case 1: + n1, err := strconv.Atoi(token[0]) + if err != nil { + continue + } + csbi.cursorPosition.y = short(n1 - 1) + case 2: + n1, err := strconv.Atoi(token[0]) + if err != nil { + continue + } + n2, err := strconv.Atoi(token[1]) + if err != nil { + continue + } + csbi.cursorPosition.x = short(n2 - 1) + csbi.cursorPosition.y = short(n1 - 1) + } + } else { + csbi.cursorPosition.y = 0 + } + procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) + case 'J': + n := 0 + if buf.Len() > 0 { + n, err = strconv.Atoi(buf.String()) + if err != nil { + continue + } + } + var count, written dword + var cursor coord + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + switch n { + case 0: + cursor = coord{x: csbi.cursorPosition.x, y: csbi.cursorPosition.y} + count = dword(csbi.size.x) - dword(csbi.cursorPosition.x) + dword(csbi.size.y-csbi.cursorPosition.y)*dword(csbi.size.x) + case 1: + cursor = coord{x: csbi.window.left, y: csbi.window.top} + count = dword(csbi.size.x) - dword(csbi.cursorPosition.x) + dword(csbi.window.top-csbi.cursorPosition.y)*dword(csbi.size.x) + case 2: + cursor = coord{x: csbi.window.left, y: csbi.window.top} + count = dword(csbi.size.x) - dword(csbi.cursorPosition.x) + dword(csbi.size.y-csbi.cursorPosition.y)*dword(csbi.size.x) + } + procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) + procFillConsoleOutputAttribute.Call(uintptr(handle), uintptr(csbi.attributes), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) + case 'K': + n := 0 + if buf.Len() > 0 { + n, err = strconv.Atoi(buf.String()) + if err != nil { + continue + } + } + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + var cursor coord + var count, written dword + switch n { + case 0: + cursor = coord{x: csbi.cursorPosition.x, y: csbi.cursorPosition.y} + count = dword(csbi.size.x - csbi.cursorPosition.x) + case 1: + cursor = coord{x: csbi.window.left, y: csbi.cursorPosition.y} + count = dword(csbi.size.x - csbi.cursorPosition.x) + case 2: + cursor = coord{x: csbi.window.left, y: csbi.cursorPosition.y} + count = dword(csbi.size.x) + } + procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) + procFillConsoleOutputAttribute.Call(uintptr(handle), uintptr(csbi.attributes), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) + case 'X': + n := 0 + if buf.Len() > 0 { + n, err = strconv.Atoi(buf.String()) + if err != nil { + continue + } + } + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + var cursor coord + var written dword + cursor = coord{x: csbi.cursorPosition.x, y: csbi.cursorPosition.y} + procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(n), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) + procFillConsoleOutputAttribute.Call(uintptr(handle), uintptr(csbi.attributes), uintptr(n), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) + case 'm': + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + attr := csbi.attributes + cs := buf.String() + if cs == "" { + procSetConsoleTextAttribute.Call(uintptr(handle), uintptr(w.oldattr)) + continue + } + token := strings.Split(cs, ";") + for i := 0; i < len(token); i++ { + ns := token[i] + if n, err = strconv.Atoi(ns); err == nil { + switch { + case n == 0 || n == 100: + attr = w.oldattr + case n == 4: + attr |= commonLvbUnderscore + case (1 <= n && n <= 3) || n == 5: + attr |= foregroundIntensity + case n == 7 || n == 27: + attr = + (attr &^ (foregroundMask | backgroundMask)) | + ((attr & foregroundMask) << 4) | + ((attr & backgroundMask) >> 4) + case n == 22: + attr &^= foregroundIntensity + case n == 24: + attr &^= commonLvbUnderscore + case 30 <= n && n <= 37: + attr &= backgroundMask + if (n-30)&1 != 0 { + attr |= foregroundRed + } + if (n-30)&2 != 0 { + attr |= foregroundGreen + } + if (n-30)&4 != 0 { + attr |= foregroundBlue + } + case n == 38: // set foreground color. + if i < len(token)-2 && (token[i+1] == "5" || token[i+1] == "05") { + if n256, err := strconv.Atoi(token[i+2]); err == nil { + if n256foreAttr == nil { + n256setup() + } + attr &= backgroundMask + attr |= n256foreAttr[n256%len(n256foreAttr)] + i += 2 + } + } else if len(token) == 5 && token[i+1] == "2" { + var r, g, b int + r, _ = strconv.Atoi(token[i+2]) + g, _ = strconv.Atoi(token[i+3]) + b, _ = strconv.Atoi(token[i+4]) + i += 4 + if r > 127 { + attr |= foregroundRed + } + if g > 127 { + attr |= foregroundGreen + } + if b > 127 { + attr |= foregroundBlue + } + } else { + attr = attr & (w.oldattr & backgroundMask) + } + case n == 39: // reset foreground color. + attr &= backgroundMask + attr |= w.oldattr & foregroundMask + case 40 <= n && n <= 47: + attr &= foregroundMask + if (n-40)&1 != 0 { + attr |= backgroundRed + } + if (n-40)&2 != 0 { + attr |= backgroundGreen + } + if (n-40)&4 != 0 { + attr |= backgroundBlue + } + case n == 48: // set background color. + if i < len(token)-2 && token[i+1] == "5" { + if n256, err := strconv.Atoi(token[i+2]); err == nil { + if n256backAttr == nil { + n256setup() + } + attr &= foregroundMask + attr |= n256backAttr[n256%len(n256backAttr)] + i += 2 + } + } else if len(token) == 5 && token[i+1] == "2" { + var r, g, b int + r, _ = strconv.Atoi(token[i+2]) + g, _ = strconv.Atoi(token[i+3]) + b, _ = strconv.Atoi(token[i+4]) + i += 4 + if r > 127 { + attr |= backgroundRed + } + if g > 127 { + attr |= backgroundGreen + } + if b > 127 { + attr |= backgroundBlue + } + } else { + attr = attr & (w.oldattr & foregroundMask) + } + case n == 49: // reset foreground color. + attr &= foregroundMask + attr |= w.oldattr & backgroundMask + case 90 <= n && n <= 97: + attr = (attr & backgroundMask) + attr |= foregroundIntensity + if (n-90)&1 != 0 { + attr |= foregroundRed + } + if (n-90)&2 != 0 { + attr |= foregroundGreen + } + if (n-90)&4 != 0 { + attr |= foregroundBlue + } + case 100 <= n && n <= 107: + attr = (attr & foregroundMask) + attr |= backgroundIntensity + if (n-100)&1 != 0 { + attr |= backgroundRed + } + if (n-100)&2 != 0 { + attr |= backgroundGreen + } + if (n-100)&4 != 0 { + attr |= backgroundBlue + } + } + procSetConsoleTextAttribute.Call(uintptr(handle), uintptr(attr)) + } + } + case 'h': + var ci consoleCursorInfo + cs := buf.String() + if cs == "5>" { + procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) + ci.visible = 0 + procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) + } else if cs == "?25" { + procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) + ci.visible = 1 + procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) + } else if cs == "?1049" { + if w.althandle == 0 { + h, _, _ := procCreateConsoleScreenBuffer.Call(uintptr(genericRead|genericWrite), 0, 0, uintptr(consoleTextmodeBuffer), 0, 0) + w.althandle = syscall.Handle(h) + if w.althandle != 0 { + handle = w.althandle + } + } + } + case 'l': + var ci consoleCursorInfo + cs := buf.String() + if cs == "5>" { + procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) + ci.visible = 1 + procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) + } else if cs == "?25" { + procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) + ci.visible = 0 + procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) + } else if cs == "?1049" { + if w.althandle != 0 { + syscall.CloseHandle(w.althandle) + w.althandle = 0 + handle = w.handle + } + } + case 's': + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + w.oldpos = csbi.cursorPosition + case 'u': + procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&w.oldpos))) + } + } + + return len(data), nil +} + +type consoleColor struct { + rgb int + red bool + green bool + blue bool + intensity bool +} + +func (c consoleColor) foregroundAttr() (attr word) { + if c.red { + attr |= foregroundRed + } + if c.green { + attr |= foregroundGreen + } + if c.blue { + attr |= foregroundBlue + } + if c.intensity { + attr |= foregroundIntensity + } + return +} + +func (c consoleColor) backgroundAttr() (attr word) { + if c.red { + attr |= backgroundRed + } + if c.green { + attr |= backgroundGreen + } + if c.blue { + attr |= backgroundBlue + } + if c.intensity { + attr |= backgroundIntensity + } + return +} + +var color16 = []consoleColor{ + {0x000000, false, false, false, false}, + {0x000080, false, false, true, false}, + {0x008000, false, true, false, false}, + {0x008080, false, true, true, false}, + {0x800000, true, false, false, false}, + {0x800080, true, false, true, false}, + {0x808000, true, true, false, false}, + {0xc0c0c0, true, true, true, false}, + {0x808080, false, false, false, true}, + {0x0000ff, false, false, true, true}, + {0x00ff00, false, true, false, true}, + {0x00ffff, false, true, true, true}, + {0xff0000, true, false, false, true}, + {0xff00ff, true, false, true, true}, + {0xffff00, true, true, false, true}, + {0xffffff, true, true, true, true}, +} + +type hsv struct { + h, s, v float32 +} + +func (a hsv) dist(b hsv) float32 { + dh := a.h - b.h + switch { + case dh > 0.5: + dh = 1 - dh + case dh < -0.5: + dh = -1 - dh + } + ds := a.s - b.s + dv := a.v - b.v + return float32(math.Sqrt(float64(dh*dh + ds*ds + dv*dv))) +} + +func toHSV(rgb int) hsv { + r, g, b := float32((rgb&0xFF0000)>>16)/256.0, + float32((rgb&0x00FF00)>>8)/256.0, + float32(rgb&0x0000FF)/256.0 + min, max := minmax3f(r, g, b) + h := max - min + if h > 0 { + if max == r { + h = (g - b) / h + if h < 0 { + h += 6 + } + } else if max == g { + h = 2 + (b-r)/h + } else { + h = 4 + (r-g)/h + } + } + h /= 6.0 + s := max - min + if max != 0 { + s /= max + } + v := max + return hsv{h: h, s: s, v: v} +} + +type hsvTable []hsv + +func toHSVTable(rgbTable []consoleColor) hsvTable { + t := make(hsvTable, len(rgbTable)) + for i, c := range rgbTable { + t[i] = toHSV(c.rgb) + } + return t +} + +func (t hsvTable) find(rgb int) consoleColor { + hsv := toHSV(rgb) + n := 7 + l := float32(5.0) + for i, p := range t { + d := hsv.dist(p) + if d < l { + l, n = d, i + } + } + return color16[n] +} + +func minmax3f(a, b, c float32) (min, max float32) { + if a < b { + if b < c { + return a, c + } else if a < c { + return a, b + } else { + return c, b + } + } else { + if a < c { + return b, c + } else if b < c { + return b, a + } else { + return c, a + } + } +} + +var n256foreAttr []word +var n256backAttr []word + +func n256setup() { + n256foreAttr = make([]word, 256) + n256backAttr = make([]word, 256) + t := toHSVTable(color16) + for i, rgb := range color256 { + c := t.find(rgb) + n256foreAttr[i] = c.foregroundAttr() + n256backAttr[i] = c.backgroundAttr() + } +} + +// EnableColorsStdout enable colors if possible. +func EnableColorsStdout(enabled *bool) func() { + var mode uint32 + h := os.Stdout.Fd() + if r, _, _ := procGetConsoleMode.Call(h, uintptr(unsafe.Pointer(&mode))); r != 0 { + if r, _, _ = procSetConsoleMode.Call(h, uintptr(mode|cENABLE_VIRTUAL_TERMINAL_PROCESSING)); r != 0 { + if enabled != nil { + *enabled = true + } + return func() { + procSetConsoleMode.Call(h, uintptr(mode)) + } + } + } + if enabled != nil { + *enabled = true + } + return func() {} +} diff --git a/vendor/github.com/mattn/go-colorable/go.test.sh b/vendor/github.com/mattn/go-colorable/go.test.sh new file mode 100644 index 0000000000..012162b077 --- /dev/null +++ b/vendor/github.com/mattn/go-colorable/go.test.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -e +echo "" > coverage.txt + +for d in $(go list ./... | grep -v vendor); do + go test -race -coverprofile=profile.out -covermode=atomic "$d" + if [ -f profile.out ]; then + cat profile.out >> coverage.txt + rm profile.out + fi +done diff --git a/vendor/github.com/mattn/go-colorable/noncolorable.go b/vendor/github.com/mattn/go-colorable/noncolorable.go new file mode 100644 index 0000000000..2dcb09aab8 --- /dev/null +++ b/vendor/github.com/mattn/go-colorable/noncolorable.go @@ -0,0 +1,58 @@ +package colorable + +import ( + "bytes" + "io" +) + +// NonColorable holds writer but removes escape sequence. +type NonColorable struct { + out io.Writer +} + +// NewNonColorable returns new instance of Writer which removes escape sequence from Writer. +func NewNonColorable(w io.Writer) io.Writer { + return &NonColorable{out: w} +} + +// Write writes data on console +func (w *NonColorable) Write(data []byte) (n int, err error) { + er := bytes.NewReader(data) + var bw [1]byte +loop: + for { + c1, err := er.ReadByte() + if err != nil { + break loop + } + if c1 != 0x1b { + bw[0] = c1 + _, err = w.out.Write(bw[:]) + if err != nil { + break loop + } + continue + } + c2, err := er.ReadByte() + if err != nil { + break loop + } + if c2 != 0x5b { + continue + } + + var buf bytes.Buffer + for { + c, err := er.ReadByte() + if err != nil { + break loop + } + if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' { + break + } + buf.Write([]byte(string(c))) + } + } + + return len(data), nil +} diff --git a/vendor/github.com/mattn/go-isatty/LICENSE b/vendor/github.com/mattn/go-isatty/LICENSE new file mode 100644 index 0000000000..65dc692b6b --- /dev/null +++ b/vendor/github.com/mattn/go-isatty/LICENSE @@ -0,0 +1,9 @@ +Copyright (c) Yasuhiro MATSUMOTO + +MIT License (Expat) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/mattn/go-isatty/README.md b/vendor/github.com/mattn/go-isatty/README.md new file mode 100644 index 0000000000..38418353e3 --- /dev/null +++ b/vendor/github.com/mattn/go-isatty/README.md @@ -0,0 +1,50 @@ +# go-isatty + +[![Godoc Reference](https://godoc.org/github.com/mattn/go-isatty?status.svg)](http://godoc.org/github.com/mattn/go-isatty) +[![Codecov](https://codecov.io/gh/mattn/go-isatty/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-isatty) +[![Coverage Status](https://coveralls.io/repos/github/mattn/go-isatty/badge.svg?branch=master)](https://coveralls.io/github/mattn/go-isatty?branch=master) +[![Go Report Card](https://goreportcard.com/badge/mattn/go-isatty)](https://goreportcard.com/report/mattn/go-isatty) + +isatty for golang + +## Usage + +```go +package main + +import ( + "fmt" + "github.com/mattn/go-isatty" + "os" +) + +func main() { + if isatty.IsTerminal(os.Stdout.Fd()) { + fmt.Println("Is Terminal") + } else if isatty.IsCygwinTerminal(os.Stdout.Fd()) { + fmt.Println("Is Cygwin/MSYS2 Terminal") + } else { + fmt.Println("Is Not Terminal") + } +} +``` + +## Installation + +``` +$ go get github.com/mattn/go-isatty +``` + +## License + +MIT + +## Author + +Yasuhiro Matsumoto (a.k.a mattn) + +## Thanks + +* k-takata: base idea for IsCygwinTerminal + + https://github.com/k-takata/go-iscygpty diff --git a/vendor/github.com/mattn/go-isatty/doc.go b/vendor/github.com/mattn/go-isatty/doc.go new file mode 100644 index 0000000000..17d4f90ebc --- /dev/null +++ b/vendor/github.com/mattn/go-isatty/doc.go @@ -0,0 +1,2 @@ +// Package isatty implements interface to isatty +package isatty diff --git a/vendor/github.com/mattn/go-isatty/go.test.sh b/vendor/github.com/mattn/go-isatty/go.test.sh new file mode 100644 index 0000000000..012162b077 --- /dev/null +++ b/vendor/github.com/mattn/go-isatty/go.test.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -e +echo "" > coverage.txt + +for d in $(go list ./... | grep -v vendor); do + go test -race -coverprofile=profile.out -covermode=atomic "$d" + if [ -f profile.out ]; then + cat profile.out >> coverage.txt + rm profile.out + fi +done diff --git a/vendor/github.com/mattn/go-isatty/isatty_bsd.go b/vendor/github.com/mattn/go-isatty/isatty_bsd.go new file mode 100644 index 0000000000..39bbcf00f0 --- /dev/null +++ b/vendor/github.com/mattn/go-isatty/isatty_bsd.go @@ -0,0 +1,19 @@ +//go:build (darwin || freebsd || openbsd || netbsd || dragonfly) && !appengine +// +build darwin freebsd openbsd netbsd dragonfly +// +build !appengine + +package isatty + +import "golang.org/x/sys/unix" + +// IsTerminal return true if the file descriptor is terminal. +func IsTerminal(fd uintptr) bool { + _, err := unix.IoctlGetTermios(int(fd), unix.TIOCGETA) + return err == nil +} + +// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2 +// terminal. This is also always false on this environment. +func IsCygwinTerminal(fd uintptr) bool { + return false +} diff --git a/vendor/github.com/mattn/go-isatty/isatty_others.go b/vendor/github.com/mattn/go-isatty/isatty_others.go new file mode 100644 index 0000000000..31503226f6 --- /dev/null +++ b/vendor/github.com/mattn/go-isatty/isatty_others.go @@ -0,0 +1,16 @@ +//go:build appengine || js || nacl || wasm +// +build appengine js nacl wasm + +package isatty + +// IsTerminal returns true if the file descriptor is terminal which +// is always false on js and appengine classic which is a sandboxed PaaS. +func IsTerminal(fd uintptr) bool { + return false +} + +// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2 +// terminal. This is also always false on this environment. +func IsCygwinTerminal(fd uintptr) bool { + return false +} diff --git a/vendor/github.com/mattn/go-isatty/isatty_plan9.go b/vendor/github.com/mattn/go-isatty/isatty_plan9.go new file mode 100644 index 0000000000..bae7f9bb3d --- /dev/null +++ b/vendor/github.com/mattn/go-isatty/isatty_plan9.go @@ -0,0 +1,23 @@ +//go:build plan9 +// +build plan9 + +package isatty + +import ( + "syscall" +) + +// IsTerminal returns true if the given file descriptor is a terminal. +func IsTerminal(fd uintptr) bool { + path, err := syscall.Fd2path(int(fd)) + if err != nil { + return false + } + return path == "/dev/cons" || path == "/mnt/term/dev/cons" +} + +// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2 +// terminal. This is also always false on this environment. +func IsCygwinTerminal(fd uintptr) bool { + return false +} diff --git a/vendor/github.com/mattn/go-isatty/isatty_solaris.go b/vendor/github.com/mattn/go-isatty/isatty_solaris.go new file mode 100644 index 0000000000..0c3acf2dc2 --- /dev/null +++ b/vendor/github.com/mattn/go-isatty/isatty_solaris.go @@ -0,0 +1,21 @@ +//go:build solaris && !appengine +// +build solaris,!appengine + +package isatty + +import ( + "golang.org/x/sys/unix" +) + +// IsTerminal returns true if the given file descriptor is a terminal. +// see: https://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libc/port/gen/isatty.c +func IsTerminal(fd uintptr) bool { + _, err := unix.IoctlGetTermio(int(fd), unix.TCGETA) + return err == nil +} + +// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2 +// terminal. This is also always false on this environment. +func IsCygwinTerminal(fd uintptr) bool { + return false +} diff --git a/vendor/github.com/mattn/go-isatty/isatty_tcgets.go b/vendor/github.com/mattn/go-isatty/isatty_tcgets.go new file mode 100644 index 0000000000..67787657fb --- /dev/null +++ b/vendor/github.com/mattn/go-isatty/isatty_tcgets.go @@ -0,0 +1,19 @@ +//go:build (linux || aix || zos) && !appengine +// +build linux aix zos +// +build !appengine + +package isatty + +import "golang.org/x/sys/unix" + +// IsTerminal return true if the file descriptor is terminal. +func IsTerminal(fd uintptr) bool { + _, err := unix.IoctlGetTermios(int(fd), unix.TCGETS) + return err == nil +} + +// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2 +// terminal. This is also always false on this environment. +func IsCygwinTerminal(fd uintptr) bool { + return false +} diff --git a/vendor/github.com/mattn/go-isatty/isatty_windows.go b/vendor/github.com/mattn/go-isatty/isatty_windows.go new file mode 100644 index 0000000000..8e3c99171b --- /dev/null +++ b/vendor/github.com/mattn/go-isatty/isatty_windows.go @@ -0,0 +1,125 @@ +//go:build windows && !appengine +// +build windows,!appengine + +package isatty + +import ( + "errors" + "strings" + "syscall" + "unicode/utf16" + "unsafe" +) + +const ( + objectNameInfo uintptr = 1 + fileNameInfo = 2 + fileTypePipe = 3 +) + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + ntdll = syscall.NewLazyDLL("ntdll.dll") + procGetConsoleMode = kernel32.NewProc("GetConsoleMode") + procGetFileInformationByHandleEx = kernel32.NewProc("GetFileInformationByHandleEx") + procGetFileType = kernel32.NewProc("GetFileType") + procNtQueryObject = ntdll.NewProc("NtQueryObject") +) + +func init() { + // Check if GetFileInformationByHandleEx is available. + if procGetFileInformationByHandleEx.Find() != nil { + procGetFileInformationByHandleEx = nil + } +} + +// IsTerminal return true if the file descriptor is terminal. +func IsTerminal(fd uintptr) bool { + var st uint32 + r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fd, uintptr(unsafe.Pointer(&st)), 0) + return r != 0 && e == 0 +} + +// Check pipe name is used for cygwin/msys2 pty. +// Cygwin/MSYS2 PTY has a name like: +// \{cygwin,msys}-XXXXXXXXXXXXXXXX-ptyN-{from,to}-master +func isCygwinPipeName(name string) bool { + token := strings.Split(name, "-") + if len(token) < 5 { + return false + } + + if token[0] != `\msys` && + token[0] != `\cygwin` && + token[0] != `\Device\NamedPipe\msys` && + token[0] != `\Device\NamedPipe\cygwin` { + return false + } + + if token[1] == "" { + return false + } + + if !strings.HasPrefix(token[2], "pty") { + return false + } + + if token[3] != `from` && token[3] != `to` { + return false + } + + if token[4] != "master" { + return false + } + + return true +} + +// getFileNameByHandle use the undocomented ntdll NtQueryObject to get file full name from file handler +// since GetFileInformationByHandleEx is not available under windows Vista and still some old fashion +// guys are using Windows XP, this is a workaround for those guys, it will also work on system from +// Windows vista to 10 +// see https://stackoverflow.com/a/18792477 for details +func getFileNameByHandle(fd uintptr) (string, error) { + if procNtQueryObject == nil { + return "", errors.New("ntdll.dll: NtQueryObject not supported") + } + + var buf [4 + syscall.MAX_PATH]uint16 + var result int + r, _, e := syscall.Syscall6(procNtQueryObject.Addr(), 5, + fd, objectNameInfo, uintptr(unsafe.Pointer(&buf)), uintptr(2*len(buf)), uintptr(unsafe.Pointer(&result)), 0) + if r != 0 { + return "", e + } + return string(utf16.Decode(buf[4 : 4+buf[0]/2])), nil +} + +// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2 +// terminal. +func IsCygwinTerminal(fd uintptr) bool { + if procGetFileInformationByHandleEx == nil { + name, err := getFileNameByHandle(fd) + if err != nil { + return false + } + return isCygwinPipeName(name) + } + + // Cygwin/msys's pty is a pipe. + ft, _, e := syscall.Syscall(procGetFileType.Addr(), 1, fd, 0, 0) + if ft != fileTypePipe || e != 0 { + return false + } + + var buf [2 + syscall.MAX_PATH]uint16 + r, _, e := syscall.Syscall6(procGetFileInformationByHandleEx.Addr(), + 4, fd, fileNameInfo, uintptr(unsafe.Pointer(&buf)), + uintptr(len(buf)*2), 0, 0) + if r == 0 || e != 0 { + return false + } + + l := *(*uint32)(unsafe.Pointer(&buf)) + return isCygwinPipeName(string(utf16.Decode(buf[2 : 2+l/2]))) +} diff --git a/vendor/github.com/pkg/errors/.gitignore b/vendor/github.com/pkg/errors/.gitignore new file mode 100644 index 0000000000..daf913b1b3 --- /dev/null +++ b/vendor/github.com/pkg/errors/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/github.com/pkg/errors/.travis.yml b/vendor/github.com/pkg/errors/.travis.yml new file mode 100644 index 0000000000..588ceca183 --- /dev/null +++ b/vendor/github.com/pkg/errors/.travis.yml @@ -0,0 +1,11 @@ +language: go +go_import_path: github.com/pkg/errors +go: + - 1.4.3 + - 1.5.4 + - 1.6.2 + - 1.7.1 + - tip + +script: + - go test -v ./... diff --git a/vendor/github.com/pkg/errors/README.md b/vendor/github.com/pkg/errors/README.md index 6483ba2afb..273db3c98a 100644 --- a/vendor/github.com/pkg/errors/README.md +++ b/vendor/github.com/pkg/errors/README.md @@ -1,4 +1,4 @@ -# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors) [![Sourcegraph](https://sourcegraph.com/github.com/pkg/errors/-/badge.svg)](https://sourcegraph.com/github.com/pkg/errors?badge) +# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors) Package errors provides simple error handling primitives. @@ -47,6 +47,6 @@ We welcome pull requests, bug fixes and issue reports. With that said, the bar f Before proposing a change, please discuss your change by raising an issue. -## License +## Licence BSD-2-Clause diff --git a/vendor/github.com/pkg/errors/stack.go b/vendor/github.com/pkg/errors/stack.go index 2874a048cf..6b1f2891a5 100644 --- a/vendor/github.com/pkg/errors/stack.go +++ b/vendor/github.com/pkg/errors/stack.go @@ -46,8 +46,7 @@ func (f Frame) line() int { // // Format accepts flags that alter the printing of some verbs, as follows: // -// %+s function name and path of source file relative to the compile time -// GOPATH separated by \n\t (\n\t) +// %+s path of source file relative to the compile time GOPATH // %+v equivalent to %+s:%d func (f Frame) Format(s fmt.State, verb rune) { switch verb { @@ -80,14 +79,6 @@ func (f Frame) Format(s fmt.State, verb rune) { // StackTrace is stack of Frames from innermost (newest) to outermost (oldest). type StackTrace []Frame -// Format formats the stack of Frames according to the fmt.Formatter interface. -// -// %s lists source files for each Frame in the stack -// %v lists the source file and line number for each Frame in the stack -// -// Format accepts flags that alter the printing of some verbs, as follows: -// -// %+v Prints filename, function, and line number for each Frame in the stack. func (st StackTrace) Format(s fmt.State, verb rune) { switch verb { case 'v': @@ -145,3 +136,43 @@ func funcname(name string) string { i = strings.Index(name, ".") return name[i+1:] } + +func trimGOPATH(name, file string) string { + // Here we want to get the source file path relative to the compile time + // GOPATH. As of Go 1.6.x there is no direct way to know the compiled + // GOPATH at runtime, but we can infer the number of path segments in the + // GOPATH. We note that fn.Name() returns the function name qualified by + // the import path, which does not include the GOPATH. Thus we can trim + // segments from the beginning of the file path until the number of path + // separators remaining is one more than the number of path separators in + // the function name. For example, given: + // + // GOPATH /home/user + // file /home/user/src/pkg/sub/file.go + // fn.Name() pkg/sub.Type.Method + // + // We want to produce: + // + // pkg/sub/file.go + // + // From this we can easily see that fn.Name() has one less path separator + // than our desired output. We count separators from the end of the file + // path until it finds two more than in the function name and then move + // one character forward to preserve the initial path segment without a + // leading separator. + const sep = "/" + goal := strings.Count(name, sep) + 2 + i := len(file) + for n := 0; n < goal; n++ { + i = strings.LastIndex(file[:i], sep) + if i == -1 { + // not enough separators found, set i so that the slice expression + // below leaves file unmodified + i = -len(sep) + break + } + } + // get back to 0 or trim the leading separator + file = file[i+len(sep):] + return file +} diff --git a/vendor/github.com/russross/blackfriday/.gitignore b/vendor/github.com/russross/blackfriday/.gitignore new file mode 100644 index 0000000000..75623dcccb --- /dev/null +++ b/vendor/github.com/russross/blackfriday/.gitignore @@ -0,0 +1,8 @@ +*.out +*.swp +*.8 +*.6 +_obj +_test* +markdown +tags diff --git a/vendor/github.com/russross/blackfriday/.travis.yml b/vendor/github.com/russross/blackfriday/.travis.yml new file mode 100644 index 0000000000..2f3351d7ae --- /dev/null +++ b/vendor/github.com/russross/blackfriday/.travis.yml @@ -0,0 +1,17 @@ +sudo: false +language: go +go: + - "1.9.x" + - "1.10.x" + - tip +matrix: + fast_finish: true + allow_failures: + - go: tip +install: + - # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step). +script: + - go get -t -v ./... + - diff -u <(echo -n) <(gofmt -d -s .) + - go tool vet . + - go test -v -race ./... diff --git a/vendor/github.com/russross/blackfriday/LICENSE.txt b/vendor/github.com/russross/blackfriday/LICENSE.txt new file mode 100644 index 0000000000..2885af3602 --- /dev/null +++ b/vendor/github.com/russross/blackfriday/LICENSE.txt @@ -0,0 +1,29 @@ +Blackfriday is distributed under the Simplified BSD License: + +> Copyright © 2011 Russ Ross +> All rights reserved. +> +> Redistribution and use in source and binary forms, with or without +> modification, are permitted provided that the following conditions +> are met: +> +> 1. Redistributions of source code must retain the above copyright +> notice, this list of conditions and the following disclaimer. +> +> 2. Redistributions in binary form must reproduce the above +> copyright notice, this list of conditions and the following +> disclaimer in the documentation and/or other materials provided with +> the distribution. +> +> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +> "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +> LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +> FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +> COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +> INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +> BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +> LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +> ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +> POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/russross/blackfriday/README.md b/vendor/github.com/russross/blackfriday/README.md new file mode 100644 index 0000000000..3c62e13753 --- /dev/null +++ b/vendor/github.com/russross/blackfriday/README.md @@ -0,0 +1,369 @@ +Blackfriday +[![Build Status][BuildSVG]][BuildURL] +[![Godoc][GodocV2SVG]][GodocV2URL] +=========== + +Blackfriday is a [Markdown][1] processor implemented in [Go][2]. It +is paranoid about its input (so you can safely feed it user-supplied +data), it is fast, it supports common extensions (tables, smart +punctuation substitutions, etc.), and it is safe for all utf-8 +(unicode) input. + +HTML output is currently supported, along with Smartypants +extensions. + +It started as a translation from C of [Sundown][3]. + + +Installation +------------ + +Blackfriday is compatible with any modern Go release. With Go and git installed: + + go get -u gopkg.in/russross/blackfriday.v2 + +will download, compile, and install the package into your `$GOPATH` directory +hierarchy. + + +Versions +-------- + +Currently maintained and recommended version of Blackfriday is `v2`. It's being +developed on its own branch: https://github.com/russross/blackfriday/tree/v2 and the +documentation is available at +https://godoc.org/gopkg.in/russross/blackfriday.v2. + +It is `go get`-able via [gopkg.in][6] at `gopkg.in/russross/blackfriday.v2`, +but we highly recommend using package management tool like [dep][7] or +[Glide][8] and make use of semantic versioning. With package management you +should import `github.com/russross/blackfriday` and specify that you're using +version 2.0.0. + +Version 2 offers a number of improvements over v1: + +* Cleaned up API +* A separate call to [`Parse`][4], which produces an abstract syntax tree for + the document +* Latest bug fixes +* Flexibility to easily add your own rendering extensions + +Potential drawbacks: + +* Our benchmarks show v2 to be slightly slower than v1. Currently in the + ballpark of around 15%. +* API breakage. If you can't afford modifying your code to adhere to the new API + and don't care too much about the new features, v2 is probably not for you. +* Several bug fixes are trailing behind and still need to be forward-ported to + v2. See issue [#348](https://github.com/russross/blackfriday/issues/348) for + tracking. + +If you are still interested in the legacy `v1`, you can import it from +`github.com/russross/blackfriday`. Documentation for the legacy v1 can be found +here: https://godoc.org/github.com/russross/blackfriday + +### Known issue with `dep` + +There is a known problem with using Blackfriday v1 _transitively_ and `dep`. +Currently `dep` prioritizes semver versions over anything else, and picks the +latest one, plus it does not apply a `[[constraint]]` specifier to transitively +pulled in packages. So if you're using something that uses Blackfriday v1, but +that something does not use `dep` yet, you will get Blackfriday v2 pulled in and +your first dependency will fail to build. + +There are couple of fixes for it, documented here: +https://github.com/golang/dep/blob/master/docs/FAQ.md#how-do-i-constrain-a-transitive-dependencys-version + +Meanwhile, `dep` team is working on a more general solution to the constraints +on transitive dependencies problem: https://github.com/golang/dep/issues/1124. + + +Usage +----- + +### v1 + +For basic usage, it is as simple as getting your input into a byte +slice and calling: + + output := blackfriday.MarkdownBasic(input) + +This renders it with no extensions enabled. To get a more useful +feature set, use this instead: + + output := blackfriday.MarkdownCommon(input) + +### v2 + +For the most sensible markdown processing, it is as simple as getting your input +into a byte slice and calling: + +```go +output := blackfriday.Run(input) +``` + +Your input will be parsed and the output rendered with a set of most popular +extensions enabled. If you want the most basic feature set, corresponding with +the bare Markdown specification, use: + +```go +output := blackfriday.Run(input, blackfriday.WithNoExtensions()) +``` + +### Sanitize untrusted content + +Blackfriday itself does nothing to protect against malicious content. If you are +dealing with user-supplied markdown, we recommend running Blackfriday's output +through HTML sanitizer such as [Bluemonday][5]. + +Here's an example of simple usage of Blackfriday together with Bluemonday: + +```go +import ( + "github.com/microcosm-cc/bluemonday" + "gopkg.in/russross/blackfriday.v2" +) + +// ... +unsafe := blackfriday.Run(input) +html := bluemonday.UGCPolicy().SanitizeBytes(unsafe) +``` + +### Custom options, v1 + +If you want to customize the set of options, first get a renderer +(currently only the HTML output engine), then use it to +call the more general `Markdown` function. For examples, see the +implementations of `MarkdownBasic` and `MarkdownCommon` in +`markdown.go`. + +### Custom options, v2 + +If you want to customize the set of options, use `blackfriday.WithExtensions`, +`blackfriday.WithRenderer` and `blackfriday.WithRefOverride`. + +### `blackfriday-tool` + +You can also check out `blackfriday-tool` for a more complete example +of how to use it. Download and install it using: + + go get github.com/russross/blackfriday-tool + +This is a simple command-line tool that allows you to process a +markdown file using a standalone program. You can also browse the +source directly on github if you are just looking for some example +code: + +* + +Note that if you have not already done so, installing +`blackfriday-tool` will be sufficient to download and install +blackfriday in addition to the tool itself. The tool binary will be +installed in `$GOPATH/bin`. This is a statically-linked binary that +can be copied to wherever you need it without worrying about +dependencies and library versions. + +### Sanitized anchor names + +Blackfriday includes an algorithm for creating sanitized anchor names +corresponding to a given input text. This algorithm is used to create +anchors for headings when `EXTENSION_AUTO_HEADER_IDS` is enabled. The +algorithm has a specification, so that other packages can create +compatible anchor names and links to those anchors. + +The specification is located at https://godoc.org/github.com/russross/blackfriday#hdr-Sanitized_Anchor_Names. + +[`SanitizedAnchorName`](https://godoc.org/github.com/russross/blackfriday#SanitizedAnchorName) exposes this functionality, and can be used to +create compatible links to the anchor names generated by blackfriday. +This algorithm is also implemented in a small standalone package at +[`github.com/shurcooL/sanitized_anchor_name`](https://godoc.org/github.com/shurcooL/sanitized_anchor_name). It can be useful for clients +that want a small package and don't need full functionality of blackfriday. + + +Features +-------- + +All features of Sundown are supported, including: + +* **Compatibility**. The Markdown v1.0.3 test suite passes with + the `--tidy` option. Without `--tidy`, the differences are + mostly in whitespace and entity escaping, where blackfriday is + more consistent and cleaner. + +* **Common extensions**, including table support, fenced code + blocks, autolinks, strikethroughs, non-strict emphasis, etc. + +* **Safety**. Blackfriday is paranoid when parsing, making it safe + to feed untrusted user input without fear of bad things + happening. The test suite stress tests this and there are no + known inputs that make it crash. If you find one, please let me + know and send me the input that does it. + + NOTE: "safety" in this context means *runtime safety only*. In order to + protect yourself against JavaScript injection in untrusted content, see + [this example](https://github.com/russross/blackfriday#sanitize-untrusted-content). + +* **Fast processing**. It is fast enough to render on-demand in + most web applications without having to cache the output. + +* **Thread safety**. You can run multiple parsers in different + goroutines without ill effect. There is no dependence on global + shared state. + +* **Minimal dependencies**. Blackfriday only depends on standard + library packages in Go. The source code is pretty + self-contained, so it is easy to add to any project, including + Google App Engine projects. + +* **Standards compliant**. Output successfully validates using the + W3C validation tool for HTML 4.01 and XHTML 1.0 Transitional. + + +Extensions +---------- + +In addition to the standard markdown syntax, this package +implements the following extensions: + +* **Intra-word emphasis supression**. The `_` character is + commonly used inside words when discussing code, so having + markdown interpret it as an emphasis command is usually the + wrong thing. Blackfriday lets you treat all emphasis markers as + normal characters when they occur inside a word. + +* **Tables**. Tables can be created by drawing them in the input + using a simple syntax: + + ``` + Name | Age + --------|------ + Bob | 27 + Alice | 23 + ``` + +* **Fenced code blocks**. In addition to the normal 4-space + indentation to mark code blocks, you can explicitly mark them + and supply a language (to make syntax highlighting simple). Just + mark it like this: + + ``` go + func getTrue() bool { + return true + } + ``` + + You can use 3 or more backticks to mark the beginning of the + block, and the same number to mark the end of the block. + + To preserve classes of fenced code blocks while using the bluemonday + HTML sanitizer, use the following policy: + + ``` go + p := bluemonday.UGCPolicy() + p.AllowAttrs("class").Matching(regexp.MustCompile("^language-[a-zA-Z0-9]+$")).OnElements("code") + html := p.SanitizeBytes(unsafe) + ``` + +* **Definition lists**. A simple definition list is made of a single-line + term followed by a colon and the definition for that term. + + Cat + : Fluffy animal everyone likes + + Internet + : Vector of transmission for pictures of cats + + Terms must be separated from the previous definition by a blank line. + +* **Footnotes**. A marker in the text that will become a superscript number; + a footnote definition that will be placed in a list of footnotes at the + end of the document. A footnote looks like this: + + This is a footnote.[^1] + + [^1]: the footnote text. + +* **Autolinking**. Blackfriday can find URLs that have not been + explicitly marked as links and turn them into links. + +* **Strikethrough**. Use two tildes (`~~`) to mark text that + should be crossed out. + +* **Hard line breaks**. With this extension enabled (it is off by + default in the `MarkdownBasic` and `MarkdownCommon` convenience + functions), newlines in the input translate into line breaks in + the output. + +* **Smart quotes**. Smartypants-style punctuation substitution is + supported, turning normal double- and single-quote marks into + curly quotes, etc. + +* **LaTeX-style dash parsing** is an additional option, where `--` + is translated into `–`, and `---` is translated into + `—`. This differs from most smartypants processors, which + turn a single hyphen into an ndash and a double hyphen into an + mdash. + +* **Smart fractions**, where anything that looks like a fraction + is translated into suitable HTML (instead of just a few special + cases like most smartypant processors). For example, `4/5` + becomes `45`, which renders as + 45. + + +Other renderers +--------------- + +Blackfriday is structured to allow alternative rendering engines. Here +are a few of note: + +* [github_flavored_markdown](https://godoc.org/github.com/shurcooL/github_flavored_markdown): + provides a GitHub Flavored Markdown renderer with fenced code block + highlighting, clickable heading anchor links. + + It's not customizable, and its goal is to produce HTML output + equivalent to the [GitHub Markdown API endpoint](https://developer.github.com/v3/markdown/#render-a-markdown-document-in-raw-mode), + except the rendering is performed locally. + +* [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt, + but for markdown. + +* [LaTeX output](https://bitbucket.org/ambrevar/blackfriday-latex): + renders output as LaTeX. + +* [bfchroma](https://github.com/Depado/bfchroma/): provides convenience + integration with the [Chroma](https://github.com/alecthomas/chroma) code + highlighting library. bfchroma is only compatible with v2 of Blackfriday and + provides a drop-in renderer ready to use with Blackfriday, as well as + options and means for further customization. + + +TODO +---- + +* More unit testing +* Improve Unicode support. It does not understand all Unicode + rules (about what constitutes a letter, a punctuation symbol, + etc.), so it may fail to detect word boundaries correctly in + some instances. It is safe on all UTF-8 input. + + +License +------- + +[Blackfriday is distributed under the Simplified BSD License](LICENSE.txt) + + + [1]: https://daringfireball.net/projects/markdown/ "Markdown" + [2]: https://golang.org/ "Go Language" + [3]: https://github.com/vmg/sundown "Sundown" + [4]: https://godoc.org/gopkg.in/russross/blackfriday.v2#Parse "Parse func" + [5]: https://github.com/microcosm-cc/bluemonday "Bluemonday" + [6]: https://labix.org/gopkg.in "gopkg.in" + [7]: https://github.com/golang/dep/ "dep" + [8]: https://github.com/Masterminds/glide "Glide" + + [BuildSVG]: https://travis-ci.org/russross/blackfriday.svg?branch=master + [BuildURL]: https://travis-ci.org/russross/blackfriday + [GodocV2SVG]: https://godoc.org/gopkg.in/russross/blackfriday.v2?status.svg + [GodocV2URL]: https://godoc.org/gopkg.in/russross/blackfriday.v2 diff --git a/vendor/github.com/russross/blackfriday/block.go b/vendor/github.com/russross/blackfriday/block.go new file mode 100644 index 0000000000..45c21a6c26 --- /dev/null +++ b/vendor/github.com/russross/blackfriday/block.go @@ -0,0 +1,1474 @@ +// +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross . +// Distributed under the Simplified BSD License. +// See README.md for details. +// + +// +// Functions to parse block-level elements. +// + +package blackfriday + +import ( + "bytes" + "strings" + "unicode" +) + +// Parse block-level data. +// Note: this function and many that it calls assume that +// the input buffer ends with a newline. +func (p *parser) block(out *bytes.Buffer, data []byte) { + if len(data) == 0 || data[len(data)-1] != '\n' { + panic("block input is missing terminating newline") + } + + // this is called recursively: enforce a maximum depth + if p.nesting >= p.maxNesting { + return + } + p.nesting++ + + // parse out one block-level construct at a time + for len(data) > 0 { + // prefixed header: + // + // # Header 1 + // ## Header 2 + // ... + // ###### Header 6 + if p.isPrefixHeader(data) { + data = data[p.prefixHeader(out, data):] + continue + } + + // block of preformatted HTML: + // + //
+ // ... + //
+ if data[0] == '<' { + if i := p.html(out, data, true); i > 0 { + data = data[i:] + continue + } + } + + // title block + // + // % stuff + // % more stuff + // % even more stuff + if p.flags&EXTENSION_TITLEBLOCK != 0 { + if data[0] == '%' { + if i := p.titleBlock(out, data, true); i > 0 { + data = data[i:] + continue + } + } + } + + // blank lines. note: returns the # of bytes to skip + if i := p.isEmpty(data); i > 0 { + data = data[i:] + continue + } + + // indented code block: + // + // func max(a, b int) int { + // if a > b { + // return a + // } + // return b + // } + if p.codePrefix(data) > 0 { + data = data[p.code(out, data):] + continue + } + + // fenced code block: + // + // ``` go info string here + // func fact(n int) int { + // if n <= 1 { + // return n + // } + // return n * fact(n-1) + // } + // ``` + if p.flags&EXTENSION_FENCED_CODE != 0 { + if i := p.fencedCodeBlock(out, data, true); i > 0 { + data = data[i:] + continue + } + } + + // horizontal rule: + // + // ------ + // or + // ****** + // or + // ______ + if p.isHRule(data) { + p.r.HRule(out) + var i int + for i = 0; data[i] != '\n'; i++ { + } + data = data[i:] + continue + } + + // block quote: + // + // > A big quote I found somewhere + // > on the web + if p.quotePrefix(data) > 0 { + data = data[p.quote(out, data):] + continue + } + + // table: + // + // Name | Age | Phone + // ------|-----|--------- + // Bob | 31 | 555-1234 + // Alice | 27 | 555-4321 + if p.flags&EXTENSION_TABLES != 0 { + if i := p.table(out, data); i > 0 { + data = data[i:] + continue + } + } + + // an itemized/unordered list: + // + // * Item 1 + // * Item 2 + // + // also works with + or - + if p.uliPrefix(data) > 0 { + data = data[p.list(out, data, 0):] + continue + } + + // a numbered/ordered list: + // + // 1. Item 1 + // 2. Item 2 + if p.oliPrefix(data) > 0 { + data = data[p.list(out, data, LIST_TYPE_ORDERED):] + continue + } + + // definition lists: + // + // Term 1 + // : Definition a + // : Definition b + // + // Term 2 + // : Definition c + if p.flags&EXTENSION_DEFINITION_LISTS != 0 { + if p.dliPrefix(data) > 0 { + data = data[p.list(out, data, LIST_TYPE_DEFINITION):] + continue + } + } + + // anything else must look like a normal paragraph + // note: this finds underlined headers, too + data = data[p.paragraph(out, data):] + } + + p.nesting-- +} + +func (p *parser) isPrefixHeader(data []byte) bool { + if data[0] != '#' { + return false + } + + if p.flags&EXTENSION_SPACE_HEADERS != 0 { + level := 0 + for level < 6 && data[level] == '#' { + level++ + } + if data[level] != ' ' { + return false + } + } + return true +} + +func (p *parser) prefixHeader(out *bytes.Buffer, data []byte) int { + level := 0 + for level < 6 && data[level] == '#' { + level++ + } + i := skipChar(data, level, ' ') + end := skipUntilChar(data, i, '\n') + skip := end + id := "" + if p.flags&EXTENSION_HEADER_IDS != 0 { + j, k := 0, 0 + // find start/end of header id + for j = i; j < end-1 && (data[j] != '{' || data[j+1] != '#'); j++ { + } + for k = j + 1; k < end && data[k] != '}'; k++ { + } + // extract header id iff found + if j < end && k < end { + id = string(data[j+2 : k]) + end = j + skip = k + 1 + for end > 0 && data[end-1] == ' ' { + end-- + } + } + } + for end > 0 && data[end-1] == '#' { + if isBackslashEscaped(data, end-1) { + break + } + end-- + } + for end > 0 && data[end-1] == ' ' { + end-- + } + if end > i { + if id == "" && p.flags&EXTENSION_AUTO_HEADER_IDS != 0 { + id = SanitizedAnchorName(string(data[i:end])) + } + work := func() bool { + p.inline(out, data[i:end]) + return true + } + p.r.Header(out, work, level, id) + } + return skip +} + +func (p *parser) isUnderlinedHeader(data []byte) int { + // test of level 1 header + if data[0] == '=' { + i := skipChar(data, 1, '=') + i = skipChar(data, i, ' ') + if data[i] == '\n' { + return 1 + } else { + return 0 + } + } + + // test of level 2 header + if data[0] == '-' { + i := skipChar(data, 1, '-') + i = skipChar(data, i, ' ') + if data[i] == '\n' { + return 2 + } else { + return 0 + } + } + + return 0 +} + +func (p *parser) titleBlock(out *bytes.Buffer, data []byte, doRender bool) int { + if data[0] != '%' { + return 0 + } + splitData := bytes.Split(data, []byte("\n")) + var i int + for idx, b := range splitData { + if !bytes.HasPrefix(b, []byte("%")) { + i = idx // - 1 + break + } + } + + data = bytes.Join(splitData[0:i], []byte("\n")) + p.r.TitleBlock(out, data) + + return len(data) +} + +func (p *parser) html(out *bytes.Buffer, data []byte, doRender bool) int { + var i, j int + + // identify the opening tag + if data[0] != '<' { + return 0 + } + curtag, tagfound := p.htmlFindTag(data[1:]) + + // handle special cases + if !tagfound { + // check for an HTML comment + if size := p.htmlComment(out, data, doRender); size > 0 { + return size + } + + // check for an
tag + if size := p.htmlHr(out, data, doRender); size > 0 { + return size + } + + // check for HTML CDATA + if size := p.htmlCDATA(out, data, doRender); size > 0 { + return size + } + + // no special case recognized + return 0 + } + + // look for an unindented matching closing tag + // followed by a blank line + found := false + /* + closetag := []byte("\n") + j = len(curtag) + 1 + for !found { + // scan for a closing tag at the beginning of a line + if skip := bytes.Index(data[j:], closetag); skip >= 0 { + j += skip + len(closetag) + } else { + break + } + + // see if it is the only thing on the line + if skip := p.isEmpty(data[j:]); skip > 0 { + // see if it is followed by a blank line/eof + j += skip + if j >= len(data) { + found = true + i = j + } else { + if skip := p.isEmpty(data[j:]); skip > 0 { + j += skip + found = true + i = j + } + } + } + } + */ + + // if not found, try a second pass looking for indented match + // but not if tag is "ins" or "del" (following original Markdown.pl) + if !found && curtag != "ins" && curtag != "del" { + i = 1 + for i < len(data) { + i++ + for i < len(data) && !(data[i-1] == '<' && data[i] == '/') { + i++ + } + + if i+2+len(curtag) >= len(data) { + break + } + + j = p.htmlFindEnd(curtag, data[i-1:]) + + if j > 0 { + i += j - 1 + found = true + break + } + } + } + + if !found { + return 0 + } + + // the end of the block has been found + if doRender { + // trim newlines + end := i + for end > 0 && data[end-1] == '\n' { + end-- + } + p.r.BlockHtml(out, data[:end]) + } + + return i +} + +func (p *parser) renderHTMLBlock(out *bytes.Buffer, data []byte, start int, doRender bool) int { + // html block needs to end with a blank line + if i := p.isEmpty(data[start:]); i > 0 { + size := start + i + if doRender { + // trim trailing newlines + end := size + for end > 0 && data[end-1] == '\n' { + end-- + } + p.r.BlockHtml(out, data[:end]) + } + return size + } + return 0 +} + +// HTML comment, lax form +func (p *parser) htmlComment(out *bytes.Buffer, data []byte, doRender bool) int { + i := p.inlineHTMLComment(out, data) + return p.renderHTMLBlock(out, data, i, doRender) +} + +// HTML CDATA section +func (p *parser) htmlCDATA(out *bytes.Buffer, data []byte, doRender bool) int { + const cdataTag = "') { + i++ + } + i++ + // no end-of-comment marker + if i >= len(data) { + return 0 + } + return p.renderHTMLBlock(out, data, i, doRender) +} + +// HR, which is the only self-closing block tag considered +func (p *parser) htmlHr(out *bytes.Buffer, data []byte, doRender bool) int { + if data[0] != '<' || (data[1] != 'h' && data[1] != 'H') || (data[2] != 'r' && data[2] != 'R') { + return 0 + } + if data[3] != ' ' && data[3] != '/' && data[3] != '>' { + // not an
tag after all; at least not a valid one + return 0 + } + + i := 3 + for data[i] != '>' && data[i] != '\n' { + i++ + } + + if data[i] == '>' { + return p.renderHTMLBlock(out, data, i+1, doRender) + } + + return 0 +} + +func (p *parser) htmlFindTag(data []byte) (string, bool) { + i := 0 + for isalnum(data[i]) { + i++ + } + key := string(data[:i]) + if _, ok := blockTags[key]; ok { + return key, true + } + return "", false +} + +func (p *parser) htmlFindEnd(tag string, data []byte) int { + // assume data[0] == '<' && data[1] == '/' already tested + + // check if tag is a match + closetag := []byte("") + if !bytes.HasPrefix(data, closetag) { + return 0 + } + i := len(closetag) + + // check that the rest of the line is blank + skip := 0 + if skip = p.isEmpty(data[i:]); skip == 0 { + return 0 + } + i += skip + skip = 0 + + if i >= len(data) { + return i + } + + if p.flags&EXTENSION_LAX_HTML_BLOCKS != 0 { + return i + } + if skip = p.isEmpty(data[i:]); skip == 0 { + // following line must be blank + return 0 + } + + return i + skip +} + +func (*parser) isEmpty(data []byte) int { + // it is okay to call isEmpty on an empty buffer + if len(data) == 0 { + return 0 + } + + var i int + for i = 0; i < len(data) && data[i] != '\n'; i++ { + if data[i] != ' ' && data[i] != '\t' { + return 0 + } + } + return i + 1 +} + +func (*parser) isHRule(data []byte) bool { + i := 0 + + // skip up to three spaces + for i < 3 && data[i] == ' ' { + i++ + } + + // look at the hrule char + if data[i] != '*' && data[i] != '-' && data[i] != '_' { + return false + } + c := data[i] + + // the whole line must be the char or whitespace + n := 0 + for data[i] != '\n' { + switch { + case data[i] == c: + n++ + case data[i] != ' ': + return false + } + i++ + } + + return n >= 3 +} + +// isFenceLine checks if there's a fence line (e.g., ``` or ``` go) at the beginning of data, +// and returns the end index if so, or 0 otherwise. It also returns the marker found. +// If syntax is not nil, it gets set to the syntax specified in the fence line. +// A final newline is mandatory to recognize the fence line, unless newlineOptional is true. +func isFenceLine(data []byte, info *string, oldmarker string, newlineOptional bool) (end int, marker string) { + i, size := 0, 0 + + // skip up to three spaces + for i < len(data) && i < 3 && data[i] == ' ' { + i++ + } + + // check for the marker characters: ~ or ` + if i >= len(data) { + return 0, "" + } + if data[i] != '~' && data[i] != '`' { + return 0, "" + } + + c := data[i] + + // the whole line must be the same char or whitespace + for i < len(data) && data[i] == c { + size++ + i++ + } + + // the marker char must occur at least 3 times + if size < 3 { + return 0, "" + } + marker = string(data[i-size : i]) + + // if this is the end marker, it must match the beginning marker + if oldmarker != "" && marker != oldmarker { + return 0, "" + } + + // TODO(shurcooL): It's probably a good idea to simplify the 2 code paths here + // into one, always get the info string, and discard it if the caller doesn't care. + if info != nil { + infoLength := 0 + i = skipChar(data, i, ' ') + + if i >= len(data) { + if newlineOptional && i == len(data) { + return i, marker + } + return 0, "" + } + + infoStart := i + + if data[i] == '{' { + i++ + infoStart++ + + for i < len(data) && data[i] != '}' && data[i] != '\n' { + infoLength++ + i++ + } + + if i >= len(data) || data[i] != '}' { + return 0, "" + } + + // strip all whitespace at the beginning and the end + // of the {} block + for infoLength > 0 && isspace(data[infoStart]) { + infoStart++ + infoLength-- + } + + for infoLength > 0 && isspace(data[infoStart+infoLength-1]) { + infoLength-- + } + + i++ + } else { + for i < len(data) && !isverticalspace(data[i]) { + infoLength++ + i++ + } + } + + *info = strings.TrimSpace(string(data[infoStart : infoStart+infoLength])) + } + + i = skipChar(data, i, ' ') + if i >= len(data) || data[i] != '\n' { + if newlineOptional && i == len(data) { + return i, marker + } + return 0, "" + } + + return i + 1, marker // Take newline into account. +} + +// fencedCodeBlock returns the end index if data contains a fenced code block at the beginning, +// or 0 otherwise. It writes to out if doRender is true, otherwise it has no side effects. +// If doRender is true, a final newline is mandatory to recognize the fenced code block. +func (p *parser) fencedCodeBlock(out *bytes.Buffer, data []byte, doRender bool) int { + var infoString string + beg, marker := isFenceLine(data, &infoString, "", false) + if beg == 0 || beg >= len(data) { + return 0 + } + + var work bytes.Buffer + + for { + // safe to assume beg < len(data) + + // check for the end of the code block + newlineOptional := !doRender + fenceEnd, _ := isFenceLine(data[beg:], nil, marker, newlineOptional) + if fenceEnd != 0 { + beg += fenceEnd + break + } + + // copy the current line + end := skipUntilChar(data, beg, '\n') + 1 + + // did we reach the end of the buffer without a closing marker? + if end >= len(data) { + return 0 + } + + // verbatim copy to the working buffer + if doRender { + work.Write(data[beg:end]) + } + beg = end + } + + if doRender { + p.r.BlockCode(out, work.Bytes(), infoString) + } + + return beg +} + +func (p *parser) table(out *bytes.Buffer, data []byte) int { + var header bytes.Buffer + i, columns := p.tableHeader(&header, data) + if i == 0 { + return 0 + } + + var body bytes.Buffer + + for i < len(data) { + pipes, rowStart := 0, i + for ; data[i] != '\n'; i++ { + if data[i] == '|' { + pipes++ + } + } + + if pipes == 0 { + i = rowStart + break + } + + // include the newline in data sent to tableRow + i++ + p.tableRow(&body, data[rowStart:i], columns, false) + } + + p.r.Table(out, header.Bytes(), body.Bytes(), columns) + + return i +} + +// check if the specified position is preceded by an odd number of backslashes +func isBackslashEscaped(data []byte, i int) bool { + backslashes := 0 + for i-backslashes-1 >= 0 && data[i-backslashes-1] == '\\' { + backslashes++ + } + return backslashes&1 == 1 +} + +func (p *parser) tableHeader(out *bytes.Buffer, data []byte) (size int, columns []int) { + i := 0 + colCount := 1 + for i = 0; data[i] != '\n'; i++ { + if data[i] == '|' && !isBackslashEscaped(data, i) { + colCount++ + } + } + + // doesn't look like a table header + if colCount == 1 { + return + } + + // include the newline in the data sent to tableRow + header := data[:i+1] + + // column count ignores pipes at beginning or end of line + if data[0] == '|' { + colCount-- + } + if i > 2 && data[i-1] == '|' && !isBackslashEscaped(data, i-1) { + colCount-- + } + + columns = make([]int, colCount) + + // move on to the header underline + i++ + if i >= len(data) { + return + } + + if data[i] == '|' && !isBackslashEscaped(data, i) { + i++ + } + i = skipChar(data, i, ' ') + + // each column header is of form: / *:?-+:? *|/ with # dashes + # colons >= 3 + // and trailing | optional on last column + col := 0 + for data[i] != '\n' { + dashes := 0 + + if data[i] == ':' { + i++ + columns[col] |= TABLE_ALIGNMENT_LEFT + dashes++ + } + for data[i] == '-' { + i++ + dashes++ + } + if data[i] == ':' { + i++ + columns[col] |= TABLE_ALIGNMENT_RIGHT + dashes++ + } + for data[i] == ' ' { + i++ + } + + // end of column test is messy + switch { + case dashes < 3: + // not a valid column + return + + case data[i] == '|' && !isBackslashEscaped(data, i): + // marker found, now skip past trailing whitespace + col++ + i++ + for data[i] == ' ' { + i++ + } + + // trailing junk found after last column + if col >= colCount && data[i] != '\n' { + return + } + + case (data[i] != '|' || isBackslashEscaped(data, i)) && col+1 < colCount: + // something else found where marker was required + return + + case data[i] == '\n': + // marker is optional for the last column + col++ + + default: + // trailing junk found after last column + return + } + } + if col != colCount { + return + } + + p.tableRow(out, header, columns, true) + size = i + 1 + return +} + +func (p *parser) tableRow(out *bytes.Buffer, data []byte, columns []int, header bool) { + i, col := 0, 0 + var rowWork bytes.Buffer + + if data[i] == '|' && !isBackslashEscaped(data, i) { + i++ + } + + for col = 0; col < len(columns) && i < len(data); col++ { + for data[i] == ' ' { + i++ + } + + cellStart := i + + for (data[i] != '|' || isBackslashEscaped(data, i)) && data[i] != '\n' { + i++ + } + + cellEnd := i + + // skip the end-of-cell marker, possibly taking us past end of buffer + i++ + + for cellEnd > cellStart && data[cellEnd-1] == ' ' { + cellEnd-- + } + + var cellWork bytes.Buffer + p.inline(&cellWork, data[cellStart:cellEnd]) + + if header { + p.r.TableHeaderCell(&rowWork, cellWork.Bytes(), columns[col]) + } else { + p.r.TableCell(&rowWork, cellWork.Bytes(), columns[col]) + } + } + + // pad it out with empty columns to get the right number + for ; col < len(columns); col++ { + if header { + p.r.TableHeaderCell(&rowWork, nil, columns[col]) + } else { + p.r.TableCell(&rowWork, nil, columns[col]) + } + } + + // silently ignore rows with too many cells + + p.r.TableRow(out, rowWork.Bytes()) +} + +// returns blockquote prefix length +func (p *parser) quotePrefix(data []byte) int { + i := 0 + for i < 3 && data[i] == ' ' { + i++ + } + if data[i] == '>' { + if data[i+1] == ' ' { + return i + 2 + } + return i + 1 + } + return 0 +} + +// blockquote ends with at least one blank line +// followed by something without a blockquote prefix +func (p *parser) terminateBlockquote(data []byte, beg, end int) bool { + if p.isEmpty(data[beg:]) <= 0 { + return false + } + if end >= len(data) { + return true + } + return p.quotePrefix(data[end:]) == 0 && p.isEmpty(data[end:]) == 0 +} + +// parse a blockquote fragment +func (p *parser) quote(out *bytes.Buffer, data []byte) int { + var raw bytes.Buffer + beg, end := 0, 0 + for beg < len(data) { + end = beg + // Step over whole lines, collecting them. While doing that, check for + // fenced code and if one's found, incorporate it altogether, + // irregardless of any contents inside it + for data[end] != '\n' { + if p.flags&EXTENSION_FENCED_CODE != 0 { + if i := p.fencedCodeBlock(out, data[end:], false); i > 0 { + // -1 to compensate for the extra end++ after the loop: + end += i - 1 + break + } + } + end++ + } + end++ + + if pre := p.quotePrefix(data[beg:]); pre > 0 { + // skip the prefix + beg += pre + } else if p.terminateBlockquote(data, beg, end) { + break + } + + // this line is part of the blockquote + raw.Write(data[beg:end]) + beg = end + } + + var cooked bytes.Buffer + p.block(&cooked, raw.Bytes()) + p.r.BlockQuote(out, cooked.Bytes()) + return end +} + +// returns prefix length for block code +func (p *parser) codePrefix(data []byte) int { + if data[0] == ' ' && data[1] == ' ' && data[2] == ' ' && data[3] == ' ' { + return 4 + } + return 0 +} + +func (p *parser) code(out *bytes.Buffer, data []byte) int { + var work bytes.Buffer + + i := 0 + for i < len(data) { + beg := i + for data[i] != '\n' { + i++ + } + i++ + + blankline := p.isEmpty(data[beg:i]) > 0 + if pre := p.codePrefix(data[beg:i]); pre > 0 { + beg += pre + } else if !blankline { + // non-empty, non-prefixed line breaks the pre + i = beg + break + } + + // verbatim copy to the working buffeu + if blankline { + work.WriteByte('\n') + } else { + work.Write(data[beg:i]) + } + } + + // trim all the \n off the end of work + workbytes := work.Bytes() + eol := len(workbytes) + for eol > 0 && workbytes[eol-1] == '\n' { + eol-- + } + if eol != len(workbytes) { + work.Truncate(eol) + } + + work.WriteByte('\n') + + p.r.BlockCode(out, work.Bytes(), "") + + return i +} + +// returns unordered list item prefix +func (p *parser) uliPrefix(data []byte) int { + i := 0 + + // start with up to 3 spaces + for i < 3 && data[i] == ' ' { + i++ + } + + // need a *, +, or - followed by a space + if (data[i] != '*' && data[i] != '+' && data[i] != '-') || + data[i+1] != ' ' { + return 0 + } + return i + 2 +} + +// returns ordered list item prefix +func (p *parser) oliPrefix(data []byte) int { + i := 0 + + // start with up to 3 spaces + for i < 3 && data[i] == ' ' { + i++ + } + + // count the digits + start := i + for data[i] >= '0' && data[i] <= '9' { + i++ + } + + // we need >= 1 digits followed by a dot and a space + if start == i || data[i] != '.' || data[i+1] != ' ' { + return 0 + } + return i + 2 +} + +// returns definition list item prefix +func (p *parser) dliPrefix(data []byte) int { + i := 0 + + // need a : followed by a spaces + if data[i] != ':' || data[i+1] != ' ' { + return 0 + } + for data[i] == ' ' { + i++ + } + return i + 2 +} + +// parse ordered or unordered list block +func (p *parser) list(out *bytes.Buffer, data []byte, flags int) int { + i := 0 + flags |= LIST_ITEM_BEGINNING_OF_LIST + work := func() bool { + for i < len(data) { + skip := p.listItem(out, data[i:], &flags) + i += skip + + if skip == 0 || flags&LIST_ITEM_END_OF_LIST != 0 { + break + } + flags &= ^LIST_ITEM_BEGINNING_OF_LIST + } + return true + } + + p.r.List(out, work, flags) + return i +} + +// Parse a single list item. +// Assumes initial prefix is already removed if this is a sublist. +func (p *parser) listItem(out *bytes.Buffer, data []byte, flags *int) int { + // keep track of the indentation of the first line + itemIndent := 0 + for itemIndent < 3 && data[itemIndent] == ' ' { + itemIndent++ + } + + i := p.uliPrefix(data) + if i == 0 { + i = p.oliPrefix(data) + } + if i == 0 { + i = p.dliPrefix(data) + // reset definition term flag + if i > 0 { + *flags &= ^LIST_TYPE_TERM + } + } + if i == 0 { + // if in defnition list, set term flag and continue + if *flags&LIST_TYPE_DEFINITION != 0 { + *flags |= LIST_TYPE_TERM + } else { + return 0 + } + } + + // skip leading whitespace on first line + for data[i] == ' ' { + i++ + } + + // find the end of the line + line := i + for i > 0 && data[i-1] != '\n' { + i++ + } + + // get working buffer + var raw bytes.Buffer + + // put the first line into the working buffer + raw.Write(data[line:i]) + line = i + + // process the following lines + containsBlankLine := false + sublist := 0 + codeBlockMarker := "" + +gatherlines: + for line < len(data) { + i++ + + // find the end of this line + for data[i-1] != '\n' { + i++ + } + + // if it is an empty line, guess that it is part of this item + // and move on to the next line + if p.isEmpty(data[line:i]) > 0 { + containsBlankLine = true + raw.Write(data[line:i]) + line = i + continue + } + + // calculate the indentation + indent := 0 + for indent < 4 && line+indent < i && data[line+indent] == ' ' { + indent++ + } + + chunk := data[line+indent : i] + + if p.flags&EXTENSION_FENCED_CODE != 0 { + // determine if in or out of codeblock + // if in codeblock, ignore normal list processing + _, marker := isFenceLine(chunk, nil, codeBlockMarker, false) + if marker != "" { + if codeBlockMarker == "" { + // start of codeblock + codeBlockMarker = marker + } else { + // end of codeblock. + *flags |= LIST_ITEM_CONTAINS_BLOCK + codeBlockMarker = "" + } + } + // we are in a codeblock, write line, and continue + if codeBlockMarker != "" || marker != "" { + raw.Write(data[line+indent : i]) + line = i + continue gatherlines + } + } + + // evaluate how this line fits in + switch { + // is this a nested list item? + case (p.uliPrefix(chunk) > 0 && !p.isHRule(chunk)) || + p.oliPrefix(chunk) > 0 || + p.dliPrefix(chunk) > 0: + + if containsBlankLine { + // end the list if the type changed after a blank line + if indent <= itemIndent && + ((*flags&LIST_TYPE_ORDERED != 0 && p.uliPrefix(chunk) > 0) || + (*flags&LIST_TYPE_ORDERED == 0 && p.oliPrefix(chunk) > 0)) { + + *flags |= LIST_ITEM_END_OF_LIST + break gatherlines + } + *flags |= LIST_ITEM_CONTAINS_BLOCK + } + + // to be a nested list, it must be indented more + // if not, it is the next item in the same list + if indent <= itemIndent { + break gatherlines + } + + // is this the first item in the nested list? + if sublist == 0 { + sublist = raw.Len() + } + + // is this a nested prefix header? + case p.isPrefixHeader(chunk): + // if the header is not indented, it is not nested in the list + // and thus ends the list + if containsBlankLine && indent < 4 { + *flags |= LIST_ITEM_END_OF_LIST + break gatherlines + } + *flags |= LIST_ITEM_CONTAINS_BLOCK + + // anything following an empty line is only part + // of this item if it is indented 4 spaces + // (regardless of the indentation of the beginning of the item) + case containsBlankLine && indent < 4: + if *flags&LIST_TYPE_DEFINITION != 0 && i < len(data)-1 { + // is the next item still a part of this list? + next := i + for data[next] != '\n' { + next++ + } + for next < len(data)-1 && data[next] == '\n' { + next++ + } + if i < len(data)-1 && data[i] != ':' && data[next] != ':' { + *flags |= LIST_ITEM_END_OF_LIST + } + } else { + *flags |= LIST_ITEM_END_OF_LIST + } + break gatherlines + + // a blank line means this should be parsed as a block + case containsBlankLine: + *flags |= LIST_ITEM_CONTAINS_BLOCK + } + + containsBlankLine = false + + // add the line into the working buffer without prefix + raw.Write(data[line+indent : i]) + + line = i + } + + // If reached end of data, the Renderer.ListItem call we're going to make below + // is definitely the last in the list. + if line >= len(data) { + *flags |= LIST_ITEM_END_OF_LIST + } + + rawBytes := raw.Bytes() + + // render the contents of the list item + var cooked bytes.Buffer + if *flags&LIST_ITEM_CONTAINS_BLOCK != 0 && *flags&LIST_TYPE_TERM == 0 { + // intermediate render of block item, except for definition term + if sublist > 0 { + p.block(&cooked, rawBytes[:sublist]) + p.block(&cooked, rawBytes[sublist:]) + } else { + p.block(&cooked, rawBytes) + } + } else { + // intermediate render of inline item + if sublist > 0 { + p.inline(&cooked, rawBytes[:sublist]) + p.block(&cooked, rawBytes[sublist:]) + } else { + p.inline(&cooked, rawBytes) + } + } + + // render the actual list item + cookedBytes := cooked.Bytes() + parsedEnd := len(cookedBytes) + + // strip trailing newlines + for parsedEnd > 0 && cookedBytes[parsedEnd-1] == '\n' { + parsedEnd-- + } + p.r.ListItem(out, cookedBytes[:parsedEnd], *flags) + + return line +} + +// render a single paragraph that has already been parsed out +func (p *parser) renderParagraph(out *bytes.Buffer, data []byte) { + if len(data) == 0 { + return + } + + // trim leading spaces + beg := 0 + for data[beg] == ' ' { + beg++ + } + + // trim trailing newline + end := len(data) - 1 + + // trim trailing spaces + for end > beg && data[end-1] == ' ' { + end-- + } + + work := func() bool { + p.inline(out, data[beg:end]) + return true + } + p.r.Paragraph(out, work) +} + +func (p *parser) paragraph(out *bytes.Buffer, data []byte) int { + // prev: index of 1st char of previous line + // line: index of 1st char of current line + // i: index of cursor/end of current line + var prev, line, i int + + // keep going until we find something to mark the end of the paragraph + for i < len(data) { + // mark the beginning of the current line + prev = line + current := data[i:] + line = i + + // did we find a blank line marking the end of the paragraph? + if n := p.isEmpty(current); n > 0 { + // did this blank line followed by a definition list item? + if p.flags&EXTENSION_DEFINITION_LISTS != 0 { + if i < len(data)-1 && data[i+1] == ':' { + return p.list(out, data[prev:], LIST_TYPE_DEFINITION) + } + } + + p.renderParagraph(out, data[:i]) + return i + n + } + + // an underline under some text marks a header, so our paragraph ended on prev line + if i > 0 { + if level := p.isUnderlinedHeader(current); level > 0 { + // render the paragraph + p.renderParagraph(out, data[:prev]) + + // ignore leading and trailing whitespace + eol := i - 1 + for prev < eol && data[prev] == ' ' { + prev++ + } + for eol > prev && data[eol-1] == ' ' { + eol-- + } + + // render the header + // this ugly double closure avoids forcing variables onto the heap + work := func(o *bytes.Buffer, pp *parser, d []byte) func() bool { + return func() bool { + pp.inline(o, d) + return true + } + }(out, p, data[prev:eol]) + + id := "" + if p.flags&EXTENSION_AUTO_HEADER_IDS != 0 { + id = SanitizedAnchorName(string(data[prev:eol])) + } + + p.r.Header(out, work, level, id) + + // find the end of the underline + for data[i] != '\n' { + i++ + } + return i + } + } + + // if the next line starts a block of HTML, then the paragraph ends here + if p.flags&EXTENSION_LAX_HTML_BLOCKS != 0 { + if data[i] == '<' && p.html(out, current, false) > 0 { + // rewind to before the HTML block + p.renderParagraph(out, data[:i]) + return i + } + } + + // if there's a prefixed header or a horizontal rule after this, paragraph is over + if p.isPrefixHeader(current) || p.isHRule(current) { + p.renderParagraph(out, data[:i]) + return i + } + + // if there's a fenced code block, paragraph is over + if p.flags&EXTENSION_FENCED_CODE != 0 { + if p.fencedCodeBlock(out, current, false) > 0 { + p.renderParagraph(out, data[:i]) + return i + } + } + + // if there's a definition list item, prev line is a definition term + if p.flags&EXTENSION_DEFINITION_LISTS != 0 { + if p.dliPrefix(current) != 0 { + return p.list(out, data[prev:], LIST_TYPE_DEFINITION) + } + } + + // if there's a list after this, paragraph is over + if p.flags&EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK != 0 { + if p.uliPrefix(current) != 0 || + p.oliPrefix(current) != 0 || + p.quotePrefix(current) != 0 || + p.codePrefix(current) != 0 { + p.renderParagraph(out, data[:i]) + return i + } + } + + // otherwise, scan to the beginning of the next line + for data[i] != '\n' { + i++ + } + i++ + } + + p.renderParagraph(out, data[:i]) + return i +} + +// SanitizedAnchorName returns a sanitized anchor name for the given text. +// +// It implements the algorithm specified in the package comment. +func SanitizedAnchorName(text string) string { + var anchorName []rune + futureDash := false + for _, r := range text { + switch { + case unicode.IsLetter(r) || unicode.IsNumber(r): + if futureDash && len(anchorName) > 0 { + anchorName = append(anchorName, '-') + } + futureDash = false + anchorName = append(anchorName, unicode.ToLower(r)) + default: + futureDash = true + } + } + return string(anchorName) +} diff --git a/vendor/github.com/russross/blackfriday/doc.go b/vendor/github.com/russross/blackfriday/doc.go new file mode 100644 index 0000000000..9656c42a19 --- /dev/null +++ b/vendor/github.com/russross/blackfriday/doc.go @@ -0,0 +1,32 @@ +// Package blackfriday is a Markdown processor. +// +// It translates plain text with simple formatting rules into HTML or LaTeX. +// +// Sanitized Anchor Names +// +// Blackfriday includes an algorithm for creating sanitized anchor names +// corresponding to a given input text. This algorithm is used to create +// anchors for headings when EXTENSION_AUTO_HEADER_IDS is enabled. The +// algorithm is specified below, so that other packages can create +// compatible anchor names and links to those anchors. +// +// The algorithm iterates over the input text, interpreted as UTF-8, +// one Unicode code point (rune) at a time. All runes that are letters (category L) +// or numbers (category N) are considered valid characters. They are mapped to +// lower case, and included in the output. All other runes are considered +// invalid characters. Invalid characters that preceed the first valid character, +// as well as invalid character that follow the last valid character +// are dropped completely. All other sequences of invalid characters +// between two valid characters are replaced with a single dash character '-'. +// +// SanitizedAnchorName exposes this functionality, and can be used to +// create compatible links to the anchor names generated by blackfriday. +// This algorithm is also implemented in a small standalone package at +// github.com/shurcooL/sanitized_anchor_name. It can be useful for clients +// that want a small package and don't need full functionality of blackfriday. +package blackfriday + +// NOTE: Keep Sanitized Anchor Name algorithm in sync with package +// github.com/shurcooL/sanitized_anchor_name. +// Otherwise, users of sanitized_anchor_name will get anchor names +// that are incompatible with those generated by blackfriday. diff --git a/vendor/github.com/russross/blackfriday/html.go b/vendor/github.com/russross/blackfriday/html.go new file mode 100644 index 0000000000..e0a6c69c96 --- /dev/null +++ b/vendor/github.com/russross/blackfriday/html.go @@ -0,0 +1,938 @@ +// +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross . +// Distributed under the Simplified BSD License. +// See README.md for details. +// + +// +// +// HTML rendering backend +// +// + +package blackfriday + +import ( + "bytes" + "fmt" + "regexp" + "strconv" + "strings" +) + +// Html renderer configuration options. +const ( + HTML_SKIP_HTML = 1 << iota // skip preformatted HTML blocks + HTML_SKIP_STYLE // skip embedded