├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin └── cloudstack-cli ├── cloudstack-cli.gemspec ├── completions └── cloudstack-cli.bash ├── examples ├── ma-test.yml ├── stack-dev.yml ├── stack_example.json ├── stack_example.yml └── stack_example_b.yml ├── lib ├── cloudstack-cli.rb └── cloudstack-cli │ ├── base.rb │ ├── cli.rb │ ├── commands │ ├── account.rb │ ├── affinity_group.rb │ ├── capacity.rb │ ├── cluster.rb │ ├── compute_offer.rb │ ├── configuration.rb │ ├── disk_offer.rb │ ├── domain.rb │ ├── environment.rb │ ├── host.rb │ ├── ip_address.rb │ ├── iso.rb │ ├── job.rb │ ├── load_balancer.rb │ ├── network.rb │ ├── network_offer.rb │ ├── physical_network.rb │ ├── pod.rb │ ├── port_rule.rb │ ├── project.rb │ ├── region.rb │ ├── resource_limit.rb │ ├── router.rb │ ├── snapshot.rb │ ├── ssh_key_pair.rb │ ├── stack.rb │ ├── storage_pool.rb │ ├── system_vm.rb │ ├── template.rb │ ├── user.rb │ ├── virtual_machine.rb │ ├── volume.rb │ └── zone.rb │ ├── helper.rb │ ├── option_resolver.rb │ ├── thor_patch.rb │ └── version.rb └── spec ├── cloudstack-cli └── commands │ ├── template_spec.rb │ └── virtual_machine_spec.rb ├── fixtures └── stack.yml └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | pkg 3 | *.gem 4 | .bundle 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # group :development do 4 | # gem 'cloudstack_client', 5 | # path: '../cloudstack_client/', 6 | # branch: 'master' 7 | # end 8 | 9 | # Specify your gem's dependencies in cloudstack-cli.gemspec 10 | gemspec 11 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | cloudstack-cli (1.6.10) 5 | cloudstack_client (~> 1.5.10) 6 | thor (~> 1.1.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | cloudstack_client (1.5.10) 12 | minitest (5.25.4) 13 | rake (13.2.1) 14 | thor (1.1.0) 15 | 16 | PLATFORMS 17 | x86_64-linux 18 | 19 | DEPENDENCIES 20 | cloudstack-cli! 21 | minitest (~> 5.11) 22 | rake (~> 13.0) 23 | 24 | BUNDLED WITH 25 | 2.5.22 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2018 Nik Wolfgramm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cloudstack CLI 2 | 3 | [![Gem Version](https://badge.fury.io/rb/cloudstack-cli.png)](http://badge.fury.io/rb/cloudstack-cli) 4 | 5 | cloudstack-cli is a [CloudStack](http://cloudstack.apache.org/) API command line client written in Ruby. 6 | cloudstack-cli uses the [cloudstack_client](https://github.com/niwo/cloudstack_client) to talk to the CloudStack API. 7 | 8 | ## Installation 9 | 10 | Install the cloudstack-cli gem: 11 | 12 | ```bash 13 | $ gem install cloudstack-cli 14 | ``` 15 | 16 | ## Setup 17 | 18 | ### Create a cloudstack-cli environment 19 | 20 | cloudstack-cli expects to find a configuration file with the API URL and your CloudStack credentials in your home directory named .cloudstack.yml (or .cloudstack-cli.yml). 21 | If the file is located elsewhere you can specify the location using the --config-file option. 22 | 23 | *Create your initial environment, which defines your connection options:* 24 | 25 | ```bash 26 | $ cloudstack-cli setup 27 | ``` 28 | 29 | "cloudstack-cli setup" (or "cloudstack-cli environment add") requires the following options: 30 | - The full URL of your CloudStack API, i.e. "https://cloud.local/client/api" 31 | - Your API Key (generate it under Accounts > Users if not already available) 32 | - Your Secret Key (see above) 33 | 34 | *Add an additional environment:* 35 | 36 | ```bash 37 | $ cloudstack-cli env add production 38 | ``` 39 | 40 | cloudstack-cli supports multiple environments using the --environment option. 41 | 42 | The first environment added is always the default. You can change the default as soon as you have multiple environments: 43 | 44 | ```bash 45 | $ cloudstack-cli environment default [environment-name] 46 | ``` 47 | 48 | *List all environments:* 49 | 50 | see `cloudstack-cli help environment` for more options. 51 | 52 | Example configuration file: 53 | 54 | ```yaml 55 | # default environment 56 | :default: production 57 | 58 | # production environment 59 | production: 60 | :url: "https://my-cloudstack-server/client/api/" 61 | :api_key: "cloudstack-api-key" 62 | :secret_key: "cloudstack-api-secret" 63 | 64 | # test environment 65 | test: 66 | :url: "http://my-cloudstack-testserver/client/api/" 67 | :api_key: "cloudstack-api-key" 68 | :secret_key: "cloudstack-api-secret" 69 | ``` 70 | 71 | ### Shell tab auto-completion 72 | 73 | To enable tab auto-completion for cloudstack-cli, add the following lines to your ~/.bash_profile file. 74 | 75 | ```bash 76 | # Bash, ~/.bash_profile 77 | eval "$(cloudstack-cli completion --shell=bash)" 78 | ``` 79 | 80 | __Note__: use `~/.bashrc` on Ubuntu 81 | 82 | ## Usage 83 | 84 | *Display the cli help:* 85 | 86 | ```bash 87 | $ cloudstack-cli help 88 | ``` 89 | 90 | *Help for a specific subcommand and command:* 91 | 92 | ```bash 93 | $ cloudstack-cli vm help 94 | ``` 95 | 96 | ```bash 97 | $ cloudstack-cli vm help list 98 | ``` 99 | 100 | ### Example: Bootstrapping a server 101 | 102 | *Bootstraps a server using a template and creating port-forwarding rules for port 22 and 80:* 103 | 104 | ```bash 105 | $ cloudstack-cli server create web-01 --template CentOS-7.5-x64 --zone DC1 --offering 2cpu_2gb --port-rules :22 :80 106 | ``` 107 | 108 | ### Example: Run any custom API command 109 | 110 | *Run the "listAlerts" command against the CloudStack API with an argument of type=8:* 111 | 112 | ```bash 113 | $ cloudstack-cli command listAlerts type=8 114 | ``` 115 | 116 | ### Example: Creating a complete stack of servers 117 | 118 | CloudStack CLI does support stack files in YAML or JSON. 119 | 120 | *An example stackfile could look like this (my_stackfile.yml):* 121 | 122 | ```yaml 123 | --- 124 | name: "web_stack-a" 125 | description: "Web Application Stack" 126 | version: "1.0" 127 | zone: "DC-ZRH-1" 128 | group: "my_web_stack" 129 | keypair: "mykeypair" 130 | servers: 131 | - name: "web-d1, web-d2" 132 | description: "web node" 133 | template: "CentOS-7-x64" 134 | offering: "1cpu_1gb" 135 | networks: "server_network" 136 | port_rules: ":80, :443" 137 | - name: "db-01" 138 | description: "PostgreSQL Master" 139 | iso: "CentOS-7-x64" 140 | disk_offering: "Perf Storage" 141 | disk_size: "5" 142 | offering: "2cpu_4gb" 143 | ip_network_list: 144 | - name: FrontendNetwork 145 | ip: 10.101.64.42 146 | - name: BackendNetwork 147 | ip: 10.102.1.11 148 | ``` 149 | 150 | *Create the stack of servers from the definition above:* 151 | 152 | ```bash 153 | $ cloudstack-cli stack create my_stackfile.yml 154 | ``` 155 | 156 | **Hint:** You can also parse a stackfile from a URI. 157 | 158 | *The following command destroys a stack using a definition gathered from a stackfile lying on a Github repository:* 159 | 160 | ```bash 161 | $ cloudstack-cli stack destroy https://raw.githubusercontent.com/niwo/cloudstack-cli/master/test/stack_example.json 162 | Destroy the following servers web-001, web-002, db-001? [y/N]: y 163 | Destroy server web-001 : job completed 164 | Destroy server web-002 : job completed 165 | Destroy server db-001 : / 166 | Completed: 2/3 (15.4s) 167 | ``` 168 | 169 | ### Example: Sort computing offerings 170 | 171 | *Sort all computing offerings by CPU and Memory grouped by domain:* 172 | (root admin privileges required) 173 | 174 | ```bash 175 | $ cloudstack-cli compute_offer sort 176 | ``` 177 | 178 | ### Example: Stop all backup routers of a given project 179 | 180 | *Stop all virtual routers of project named Demo (you could filter by zone too):* 181 | (this command is helpful if you have to deploy new major release of CloudStack when using redundant routers) 182 | 183 | ```bash 184 | $ cloudstack-cli router list --project Demo --status running --redundant-state BACKUP --command STOP 185 | ```` 186 | 187 | ## References 188 | 189 | - [Cloudstack API documentation](http://cloudstack.apache.org/docs/api/) 190 | - This tool was inspired by the Knife extension for Cloudstack: [knife-cloudstack](https://github.com/CloudStack-extras/knife-cloudstack) 191 | 192 | 193 | ## Test 194 | 195 | 1. Requires the [cloudstack-simulator](https://hub.docker.com/r/cloudstack/simulator/) docker images running on your local machine 196 | 2. You need to add the admin secrets to your local cloudstack environment an make it default 197 | 3. Currently you need to create a isolated network named "test-network" manually on the simulator 198 | 4. Run `bundle exec rake test` 199 | 200 | ## Contributing 201 | 202 | 1. Fork it 203 | 2. Create your feature branch (`git checkout -b my-new-feature`) 204 | 3. Commit your changes (`git commit -am 'Add some feature'`) 205 | 4. Push to the branch (`git push origin my-new-feature`) 206 | 5. Create new Pull Request 207 | 208 | 209 | ## License 210 | 211 | Released under the MIT License. See the [LICENSE](https://raw.github.com/niwo/cloudstack-cli/master/LICENSE.txt) file for further details. 212 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | 5 | Rake::TestTask.new do |t| 6 | t.libs << "spec" 7 | t.pattern = "spec/**/*_spec.rb" 8 | t.warning = false 9 | end 10 | 11 | task default: :test 12 | -------------------------------------------------------------------------------- /bin/cloudstack-cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'cloudstack-cli' 4 | 5 | CloudstackCli::Cli.start(ARGV) 6 | -------------------------------------------------------------------------------- /cloudstack-cli.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'cloudstack-cli/version' 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = 'cloudstack-cli' 8 | gem.version = CloudstackCli::VERSION 9 | gem.authors = ['Nik Wolfgramm'] 10 | gem.email = %w(nik.wolfgramm@gmail.com) 11 | gem.description = %q{cloudstack-cli is a CloudStack API command line client written in Ruby.} 12 | gem.summary = %q{cloudstack-cli CloudStack API client} 13 | gem.date = Time.now.utc.strftime("%Y-%m-%d") 14 | gem.homepage = 'http://github.com/niwo/cloudstack-cli' 15 | gem.license = 'MIT' 16 | 17 | gem.required_ruby_version = '>= 1.9.3' 18 | gem.files = `git ls-files`.split($/) 19 | gem.executables = %w(cloudstack-cli) 20 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 21 | gem.require_paths = %w(lib) 22 | gem.rdoc_options = %w[--line-numbers --inline-source] 23 | 24 | gem.add_development_dependency('rake', '~> 13.0') 25 | gem.add_development_dependency('minitest', '~> 5.11') 26 | 27 | gem.add_dependency('cloudstack_client', '~> 1.5.10') 28 | gem.add_dependency('thor', '~> 1.1.0') 29 | end 30 | -------------------------------------------------------------------------------- /completions/cloudstack-cli.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # cloudstack-cli 4 | # https://github.com/niwo/cloudstack-cli 5 | # 6 | # Copyright (c) 2015 Nik Wolfgramm 7 | # Licensed under the MIT license. 8 | # https://raw.github.com/niwo/cloudstack-cli/master/LICENSE.txt 9 | 10 | # Usage: 11 | # 12 | # To enable bash completion for cloudstack-cli, add the following line (minus the 13 | # leading #, which is the bash comment character) to your ~/.bash_profile file (use ~/.bashrc on Ubuntu): 14 | # 15 | # eval "$(cloudstack-cli completion --shell=bash)" 16 | 17 | _cloudstack_cli() { 18 | COMPREPLY=() 19 | local word="${COMP_WORDS[COMP_CWORD]}" 20 | # list available base commands 21 | if [ "$COMP_CWORD" -eq 1 ]; then 22 | COMPREPLY=( $(compgen -W "$(cloudstack-cli help | grep cloudstack-cli | cut -d ' ' -f4)" -- "$word") ) 23 | else 24 | local words=("${COMP_WORDS[@]}") 25 | # ignore commands which contain 'help' 26 | if [[ "${words[@]}" == *help* ]]; then 27 | COMPREPLY=( $(compgen -W '' -- "$word") ) 28 | # search for subcommand 29 | elif [[ "$word" != -* ]] && [ "$COMP_CWORD" -eq 2 ]; then 30 | local cp1=$(echo ${words[@]} | cut -d ' ' -f1-2) 31 | COMPREPLY=( $(compgen -W "$($cp1 help | grep cloudstack-cli | cut -d ' ' -f5)" -- "$word") ) 32 | # list options for the subcommand 33 | elif [[ "$word" =~ -* ]] && [ "$COMP_CWORD" -gt 2 ]; then 34 | local cp1=$(echo ${words[@]} | cut -d ' ' -f1-2) 35 | local cp2=$(echo ${words[@]} | cut -d ' ' -f3) 36 | local cp3=$($cp1 help $cp2 2>/dev/null) 37 | COMPREPLY=( $(compgen -W "$(echo $cp3 | awk 'NR>1{print $1}' RS=[ FS='\=' 2>/dev/null)" -- "$word") ) 38 | else 39 | COMPREPLY=() 40 | fi 41 | return 0 42 | fi 43 | } 44 | 45 | complete -o bashdefault -o default -F _cloudstack_cli cloudstack-cli cs 46 | -------------------------------------------------------------------------------- /examples/ma-test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: test-from-templ 3 | description: Testinstall from Template 4 | version: 1.0 5 | zone: ZUERICH_IX 6 | servers: 7 | - name: test-01 8 | template: web-d2-2015-07-16 9 | offering: 1cpu_1gb 10 | networks: wolfgrni 11 | -------------------------------------------------------------------------------- /examples/stack-dev.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "web_stack_a" 3 | description: "Web Application Stack" 4 | version: "1.0" 5 | zone: "Sandbox-simulator" 6 | group: "my_web_stack" 7 | servers: 8 | - name: "web-01, web-02" 9 | description: "Web nodes" 10 | template: "CentOS 5.3(64-bit) no GUI (Simulator)" 11 | offering: "Small Instance" 12 | networks: "test-network" 13 | port_rules: ":80, :443" 14 | - name: "db-01" 15 | description: "PostgreSQL Master" 16 | template: "CentOS 5.3(64-bit) no GUI (Simulator)" 17 | offering: "Medium Instance" 18 | ip_network_list: 19 | - name: "test-network" 20 | ip: 10.1.1.11 21 | -------------------------------------------------------------------------------- /examples/stack_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web_stack_a", 3 | "description": "Web Application Stack", 4 | "version": "1.0", 5 | "zone": "ZUERICH_EQ", 6 | "project": "Playground", 7 | "group": "my_web_stack", 8 | "servers": [ 9 | { 10 | "name": "web-001, web-002", 11 | "description": "Web nodes", 12 | "template": "CentOS-7-x86_64", 13 | "offering": "1cpu_1gb", 14 | "networks": "M_PLAY", 15 | "port_rules": ":80, :443" 16 | }, 17 | { 18 | "name": "db-001", 19 | "description": "PostgreSQL Master", 20 | "iso": "CentOS-7.0-x64", 21 | "disk_offering": "Perf Storage", 22 | "disk_size": "5", 23 | "offering": "2cpu_4gb", 24 | "networks": "M_PLAY" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /examples/stack_example.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "web_stack_a" 3 | description: "Web Application Stack" 4 | version: "1.0" 5 | zone: "ZUERICH_EQ" 6 | project: "Playground" 7 | group: "my_web_stack" 8 | servers: 9 | - name: "web-001, web-002" 10 | description: "Web nodes" 11 | template: "CentOS-7-x86_64" 12 | offering: "1cpu_1gb" 13 | networks: "M_PLAY" 14 | port_rules: ":80, :443" 15 | - name: "db-001" 16 | description: "PostgreSQL Master" 17 | iso: "CentOS-7.0-x64" 18 | disk_offering: "Perf Storage" 19 | disk_size: "5" 20 | offering: "2cpu_4gb" 21 | ip_network_list: 22 | - name: "M_PLAY" 23 | ip: 10.101.64.42 24 | -------------------------------------------------------------------------------- /examples/stack_example_b.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "web_stack_b" 3 | description: "Web Application Stack" 4 | version: "1.0" 5 | zone: "ZUERICH_EQ" 6 | project: "Playground" 7 | group: "my_web_stack" 8 | servers: 9 | - name: "webx-01, webx-02" 10 | description: "Web nodes" 11 | template: "CentOS-7-x86_64" 12 | offering: "1cpu_1gb" 13 | networks: "M_PLAY" 14 | port_rules: ":80, :443" 15 | -------------------------------------------------------------------------------- /lib/cloudstack-cli.rb: -------------------------------------------------------------------------------- 1 | require "cloudstack_client" 2 | require "cloudstack_client/configuration" 3 | 4 | require "cloudstack-cli/version" 5 | require "cloudstack-cli/helper" 6 | require "cloudstack-cli/option_resolver" 7 | require "cloudstack-cli/base" 8 | require "cloudstack-cli/cli" 9 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/base.rb: -------------------------------------------------------------------------------- 1 | require "thor" 2 | require "cloudstack-cli/thor_patch" 3 | require "json" 4 | require "yaml" 5 | require "open-uri" 6 | 7 | module CloudstackCli 8 | class Base < Thor 9 | include Thor::Actions 10 | include CloudstackCli::Helper 11 | include CloudstackCli::OptionResolver 12 | 13 | attr_reader :config 14 | 15 | # rescue error globally 16 | def self.start(given_args=ARGV, config={}) 17 | super 18 | rescue => e 19 | error_class = e.class.name.split('::') 20 | if error_class.size == 2 && error_class.first == "CloudstackClient" 21 | puts "\e[31mERROR\e[0m: #{error_class.last} - #{e.message}" 22 | puts e.backtrace if ARGV.include? "--debug" 23 | else 24 | raise 25 | end 26 | end 27 | 28 | # catch control-c and exit 29 | trap("SIGINT") do 30 | puts 31 | puts "bye.." 32 | exit! 33 | end 34 | 35 | # exit with return code 1 in case of a error 36 | def self.exit_on_failure? 37 | true 38 | end 39 | 40 | no_commands do 41 | def client 42 | @config ||= load_configuration 43 | @client ||= CloudstackClient::Client.new( 44 | @config[:url], 45 | @config[:api_key], 46 | @config[:secret_key] 47 | ) 48 | @client.debug = true if options[:debug] 49 | @client 50 | end 51 | 52 | def load_configuration 53 | CloudstackClient::Configuration.load(options) 54 | rescue CloudstackClient::ConfigurationError => e 55 | say "Error: ", :red 56 | say e.message 57 | exit 1 58 | end 59 | 60 | def filter_by(objects, key, value) 61 | if objects.size == 0 62 | return objects 63 | elsif !(keys = objects.map{|i| i.keys}.flatten.uniq).include?(key) 64 | say "WARNING: Filter invalid, no key \"#{key}\" found.", :yellow 65 | say("DEBUG: Supported keys are, #{keys.join(', ')}.", :magenta) if options[:debug] 66 | return objects 67 | end 68 | objects.select do |object| 69 | object[key.to_s].to_s =~ /#{value}/i 70 | end 71 | rescue RegexpError => e 72 | say "ERROR: ", :red 73 | say "Invalid regular expression in filter - #{e.message}" 74 | exit 1 75 | end 76 | 77 | def filter_objects(objects, filter = options[:filter]) 78 | filter.each do |key, value| 79 | objects = filter_by(objects, key, value) 80 | return objects if objects.size == 0 81 | end 82 | objects 83 | end 84 | 85 | def add_filters_to_options(command) 86 | options[:filter].each do |filter_key, filter_value| 87 | if client.api.params(command).find {|param| param["name"] == filter_key.downcase } 88 | options[filter_key.downcase] = filter_value.gsub(/[^\w\s\.-]/, '') 89 | options[:filter].delete(filter_key) 90 | end 91 | end 92 | end 93 | 94 | def parse_file(file, extensions = %w(.json .yaml .yml)) 95 | handler = case File.extname(file) 96 | when ".json" 97 | Object.const_get "JSON" 98 | when ".yaml", ".yml" 99 | Object.const_get "YAML" 100 | else 101 | say "ERROR: ", :red 102 | say "File extension #{File.extname(file)} not supported. Supported extensions are #{extensions.join(', ')}" 103 | exit 104 | end 105 | begin 106 | return handler.load open(file){|f| f.read} 107 | rescue SystemCallError 108 | say "ERROR: ", :red 109 | say "Can't find the file '#{file}'." 110 | exit 1 111 | rescue => e 112 | say "ERROR: ", :red 113 | say "Can't parse file '#{file}': #{e.message}" 114 | exit 1 115 | end 116 | end 117 | end 118 | 119 | end # class 120 | end # module 121 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/cli.rb: -------------------------------------------------------------------------------- 1 | module CloudstackCli 2 | class Cli < CloudstackCli::Base 3 | include Thor::Actions 4 | 5 | package_name "cloudstack-cli" 6 | 7 | class_option :config_file, 8 | default: CloudstackClient::Configuration.locate_config_file || 9 | File.join(Dir.home, ".cloudstack.yml"), 10 | aliases: '-c', 11 | desc: 'Location of your cloudstack configuration file' 12 | 13 | class_option :env, 14 | aliases: '-e', 15 | desc: 'Environment to use' 16 | 17 | class_option :debug, 18 | desc: 'Enable debug output', 19 | type: :boolean, 20 | default: false 21 | 22 | desc "version", "Print cloudstack-cli version number" 23 | def version 24 | say "cloudstack-cli version #{CloudstackCli::VERSION}" 25 | say " (cloudstack_client version #{CloudstackClient::VERSION})" 26 | end 27 | 28 | desc "setup", "Initial configuration of Cloudstack connection settings" 29 | def setup(env = options[:environment]) 30 | invoke "environment:add", [env], 31 | :config_file => options[:config_file] 32 | end 33 | 34 | desc "completion", "Load the shell scripts for auto-completion" 35 | option :shell, default: 'bash' 36 | def completion 37 | shell_script = File.join( 38 | File.dirname(__FILE__), '..', '..', 39 | 'completions', "cloudstack-cli.#{options[:shell]}" 40 | ) 41 | unless File.file? shell_script 42 | say "Specified cloudstack-cli shell auto-completion rules for #{options[:shell]} not found.", :red 43 | exit 1 44 | end 45 | puts File.read shell_script 46 | end 47 | 48 | desc "command COMMAND [arg1=val1 arg2=val2...]", "Run a custom api command" 49 | option :format, default: 'yaml', 50 | enum: %w(json yaml), desc: "output format" 51 | option :pretty_print, default: true, type: :boolean, 52 | desc: "pretty print json output" 53 | def command(command, *args) 54 | params = { 'command' => command } 55 | args.each do |arg| 56 | arg = arg.split('=') 57 | params[arg[0]] = arg[1] 58 | end 59 | 60 | unless client.api.commands.has_key? command 61 | say "ERROR: ", :red 62 | say "Unknown API command '#{command}'." 63 | exit! 64 | end 65 | 66 | unless client.api.all_required_params?(command, params) 67 | raise CloudstackClient::ParameterError, client.api.missing_params_msg(command) 68 | end 69 | 70 | data = client.send_request(params) 71 | if options[:format] == 'json' 72 | puts options[:pretty_print] ? JSON.pretty_generate(data) : data.to_json 73 | else 74 | puts data.to_yaml 75 | end 76 | end 77 | 78 | # Require and describe subcommands 79 | Dir[File.dirname(__FILE__) + '/commands/*.rb'].each do |command_path| 80 | require command_path 81 | 82 | command = File.basename(command_path, ".rb") 83 | class_names = command.split('_').collect(&:capitalize) 84 | 85 | desc "#{command} SUBCOMMAND ...ARGS", 86 | "#{class_names.join(' ')} commands" 87 | subcommand command, 88 | Object.const_get(class_names.join) 89 | end 90 | 91 | # Additional command maps (aliases) 92 | map %w(-v --version) => :version 93 | map 'env' => :environment 94 | map 'vm' => :virtual_machine 95 | map 'server' => :virtual_machine 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/account.rb: -------------------------------------------------------------------------------- 1 | class Account < CloudstackCli::Base 2 | 3 | TYPES = { 4 | 0 => 'user', 5 | 1 => 'domain-admin', 6 | 2 => 'admin' 7 | } 8 | 9 | desc "show NAME", "show detailed infos about an account" 10 | def show(name) 11 | unless account = client.list_accounts(name: name, listall: true).first 12 | say "No account named \"#{name}\" found.", :red 13 | else 14 | account.delete 'user' 15 | account['accounttype'] = "#{account['accounttype']} (#{TYPES[account['accounttype']]})" 16 | table = account.map do |key, value| 17 | [ set_color("#{key}", :yellow), "#{value}" ] 18 | end 19 | print_table table 20 | end 21 | end 22 | 23 | desc "list", "list accounts" 24 | option :format, default: "table", 25 | enum: %w(table json yaml) 26 | def list 27 | accounts = client.list_accounts(listall: true) 28 | if accounts.size < 1 29 | puts "No accounts found." 30 | else 31 | case options[:format].to_sym 32 | when :yaml 33 | puts({accounts: accounts}.to_yaml) 34 | when :json 35 | puts JSON.pretty_generate(accounts: accounts) 36 | else 37 | table = [%w(Name Type Domain State)] 38 | accounts.each do |account| 39 | table << [ 40 | account['name'], 41 | TYPES[account['accounttype']], 42 | account['domain'], 43 | account['state'] 44 | ] 45 | end 46 | print_table table 47 | say "Total number of accounts: #{accounts.size}" 48 | end 49 | end 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/affinity_group.rb: -------------------------------------------------------------------------------- 1 | class AffinityGroup < CloudstackCli::Base 2 | 3 | desc 'list', 'list affinity groups' 4 | option :account 5 | option :name 6 | option :type 7 | option :listall, type: :boolean, default: true 8 | option :keyword 9 | option :format, default: "table", 10 | enum: %w(table json yaml) 11 | def list 12 | resolve_account 13 | affinity_groups = client.list_affinity_groups(options) 14 | if affinity_groups.size < 1 15 | say "No affinity groups found." 16 | else 17 | case options[:format].to_sym 18 | when :yaml 19 | puts({affinity_groups: affinity_groups}.to_yaml) 20 | when :json 21 | puts JSON.pretty_generate(affinity_groups: affinity_groups) 22 | else 23 | table = [%w(Domain Account Name, Description, VMs)] 24 | affinity_groups.each do |group| 25 | table << [ 26 | group['domain'], group['account'], 27 | group['name'], group['description'], 28 | group['virtualmachineIds'] ? group['virtualmachineIds'].size : nil 29 | ] 30 | end 31 | print_table table 32 | say "Total number of affinity groups: #{affinity_groups.size}" 33 | end 34 | end 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/capacity.rb: -------------------------------------------------------------------------------- 1 | class Capacity < CloudstackCli::Base 2 | CAPACITY_TYPES = { 3 | 0 => {name: "Memory", unit: "GB", divider: 1024.0**3}, 4 | 1 => {name: "CPU", unit: "GHz", divider: 1000.0}, 5 | 2 => {name: "Storage", unit: "TB", divider: 1024.0**4}, 6 | 3 => {name: "Primary Storage", unit: "TB", divider: 1024.0**4}, 7 | 4 => {name: "Public IP's"}, 8 | 5 => {name: "Private IP's"}, 9 | 6 => {name: "Secondary Storage", unit: "TB", divider: 1024.0**4}, 10 | 7 => {name: "VLAN"}, 11 | 8 => {name: "Direct Attached Public IP's"}, 12 | 9 => {name: "Local Storage", unit: "TB", divider: 1024.0**4} 13 | } 14 | 15 | desc "list", "list system capacity" 16 | option :zone, desc: "list capacity by zone" 17 | option :cluster, desc: "list capacity by cluster" 18 | option :type, desc: "specify type, see types for a list of types" 19 | def list 20 | resolve_zone 21 | resolve_cluster 22 | capacities = client.list_capacity(options) 23 | table = [] 24 | header = ["Zone", "Type", "Capacity Used", "Capacity Total", "Used"] 25 | header[0] = "Cluster" if options[:cluster_id] 26 | capacities.each do |c| 27 | if CAPACITY_TYPES.include? c['type'] 28 | table << [ 29 | c['clustername'] || c['zonename'], 30 | CAPACITY_TYPES[c['type']][:name], 31 | capacity_to_s(c, 'capacityused'), 32 | capacity_to_s(c, 'capacitytotal'), 33 | "#{c['percentused']}%" 34 | ] 35 | end 36 | end 37 | table = table.sort {|a, b| [a[0], a[1]] <=> [b[0], b[1]]} 38 | print_table table.insert(0, header) 39 | end 40 | 41 | desc "types", "show capacity types" 42 | def types 43 | table = [['type', 'name']] 44 | CAPACITY_TYPES.each_pair do |type, data| 45 | table << [type, data[:name]] 46 | end 47 | print_table table 48 | end 49 | 50 | no_commands do 51 | 52 | def capacity_to_s(capacity, entity) 53 | value = CAPACITY_TYPES[capacity['type']][:divider] ? 54 | (capacity[entity] / CAPACITY_TYPES[capacity['type']][:divider]).round(1) : 55 | capacity[entity] 56 | CAPACITY_TYPES[capacity['type']][:unit] ? 57 | "#{value} #{CAPACITY_TYPES[capacity['type']][:unit]}" : 58 | value.to_s 59 | end 60 | 61 | end 62 | 63 | end 64 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/cluster.rb: -------------------------------------------------------------------------------- 1 | class Cluster < CloudstackCli::Base 2 | 3 | desc 'list', 'list clusters' 4 | option :zone, desc: "lists clusters by zone" 5 | option :format, default: "table", 6 | enum: %w(table json yaml) 7 | def list 8 | resolve_zone if options[:zone] 9 | clusters = client.list_clusters(options) 10 | if clusters.size < 1 11 | say "No clusters found." 12 | else 13 | case options[:format].to_sym 14 | when :yaml 15 | puts({clusters: clusters}.to_yaml) 16 | when :json 17 | puts JSON.pretty_generate(clusters: clusters) 18 | else 19 | table = [%w(Name Pod_Name Type Zone State)] 20 | clusters.each do |cluster| 21 | table << [ 22 | cluster['name'], cluster['podname'], 23 | cluster['hypervisortype'], cluster['zonename'], 24 | cluster['managedstate'] 25 | ] 26 | end 27 | print_table table 28 | say "Total number of clusters: #{clusters.size}" 29 | end 30 | end 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/compute_offer.rb: -------------------------------------------------------------------------------- 1 | class ComputeOffer < CloudstackCli::Base 2 | 3 | desc 'list', 'list compute offerings' 4 | option :domain, desc: "the domain associated with the compute offering" 5 | option :format, default: "table", 6 | enum: %w(table json yaml) 7 | option :filter, type: :hash, 8 | desc: "filter objects based on arrtibutes: (attr1:regex attr2:regex ...)" 9 | def list 10 | resolve_domain 11 | add_filters_to_options("listServiceOfferings") if options[:filter] 12 | offerings = client.list_service_offerings(options) 13 | offerings = filter_objects(offerings) if options[:filter] 14 | if offerings.size < 1 15 | puts "No offerings found." 16 | else 17 | case options[:format].to_sym 18 | when :yaml 19 | puts({compute_offers: offerings}.to_yaml) 20 | when :json 21 | puts JSON.pretty_generate(compute_offers: offerings) 22 | else 23 | print_compute_offerings(offerings) 24 | end 25 | end 26 | end 27 | 28 | desc 'create NAME', 'create compute offering' 29 | option :cpunumber, required: true 30 | option :cpuspeed, required: true 31 | option :displaytext, required: true 32 | option :memory, required: true 33 | option :domain 34 | option :ha, type: :boolean 35 | option :tags 36 | option :hosttags 37 | def create(name) 38 | resolve_domain 39 | options[:name] = name 40 | say("OK", :green) if client.create_service_offering(options) 41 | end 42 | 43 | desc 'delete ID', 'delete compute offering' 44 | def delete(id) 45 | offerings = client.list_service_offerings(id: id) 46 | if offerings && offerings.size == 1 47 | say "Are you sure you want to delete compute offering below?", :yellow 48 | print_compute_offerings(offerings, false) 49 | if yes?("[y/N]:", :yellow) 50 | say("OK", :green) if client.delete_service_offering(id: id) 51 | end 52 | else 53 | say "No offering with ID #{id} found.", :yellow 54 | end 55 | end 56 | 57 | desc 'sort', 'sort by cpu and memory grouped by domain' 58 | def sort 59 | offerings = client.list_service_offerings 60 | sortkey = -1 61 | offerings.group_by{|o| o["domain"]}.each_value do |offers| 62 | offers.sort { 63 | |oa, ob| [oa["cpunumber"], oa["memory"], oa["name"]] <=> [ob["cpunumber"], ob["memory"], ob["name"]] 64 | }.each do |offer| 65 | puts "#{sortkey.abs} #{offer['domain']} - #{offer["displaytext"]}" 66 | client.update_service_offering( 67 | id: offer['id'], 68 | sortkey: sortkey 69 | ) 70 | sortkey -= 1 71 | end 72 | end 73 | end 74 | 75 | no_commands do 76 | def print_compute_offerings(offerings, totals = true) 77 | table = [%w(Name Displaytext Domain Tags HostTags ID)] 78 | offerings.each do |offering| 79 | table << [ 80 | offering["name"], 81 | offering["displaytext"], 82 | offering["domain"], 83 | offering["tags"], 84 | offering["hosttags"], 85 | offering["id"] 86 | ] 87 | end 88 | print_table table 89 | say "Total number of offerings: #{offerings.size}" if totals 90 | end 91 | end 92 | 93 | end 94 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/configuration.rb: -------------------------------------------------------------------------------- 1 | class Configuration < CloudstackCli::Base 2 | 3 | desc 'list', 'list configurations' 4 | option :name, desc: "lists configuration by name" 5 | option :category, desc: "lists configurations by category" 6 | option :keyword, desc: "lists configuration by keyword" 7 | def list 8 | configs = client.list_configurations(options) 9 | if configs.size < 1 10 | say "No configuration found." 11 | else 12 | table = [%w(Name Category Value)] 13 | configs.each do |config| 14 | table << [ 15 | config['name'], 16 | config['category'], 17 | config['value'] 18 | ] 19 | end 20 | print_table table 21 | say "Total number of configurations: #{configs.size}" 22 | end 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/disk_offer.rb: -------------------------------------------------------------------------------- 1 | class DiskOffer < CloudstackCli::Base 2 | 3 | desc 'list', 'list disk offerings' 4 | option :domain, desc: "the domain of the disk offering" 5 | option :format, default: "table", 6 | enum: %w(table json yaml) 7 | option :filter, type: :hash, 8 | desc: "filter objects based on arrtibutes: (attr1:regex attr2:regex ...)" 9 | def list 10 | resolve_domain 11 | add_filters_to_options("listDiskOfferings") if options[:filter] 12 | offerings = client.list_disk_offerings(options) 13 | offerings = filter_objects(offerings) if options[:filter] 14 | if offerings.size < 1 15 | puts "No offerings found." 16 | else 17 | case options[:format].to_sym 18 | when :yaml 19 | puts({disk_offers: offerings}.to_yaml) 20 | when :json 21 | puts JSON.pretty_generate(disk_offers: offerings) 22 | else 23 | table = [["Name", "Displaytext", "Domain", "ID"]] 24 | offerings.each do |offering| 25 | table << [ 26 | offering["name"], 27 | offering["displaytext"], 28 | offering["domain"], 29 | offering["id"] 30 | ] 31 | end 32 | print_table table 33 | say "Total number of offerings: #{offerings.size}" 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/domain.rb: -------------------------------------------------------------------------------- 1 | class Domain < CloudstackCli::Base 2 | 3 | desc 'list', 'list domains' 4 | option :format, default: "table", 5 | enum: %w(table json yaml) 6 | def list 7 | domains = client.list_domains 8 | if domains.size < 1 9 | puts "No domains found." 10 | else 11 | case options[:format].to_sym 12 | when :yaml 13 | puts({domains: domains}.to_yaml) 14 | when :json 15 | puts JSON.pretty_generate(domains: domains) 16 | else 17 | table = [%w(Name Path)] 18 | domains.each do |domain| 19 | table << [domain['name'], domain['path']] 20 | end 21 | print_table table 22 | say "Total number of domains: #{domains.size}" 23 | end 24 | end 25 | end 26 | 27 | desc 'create', 'create domain' 28 | option :network_domain, desc: "Network domain for networks in the domain." 29 | option :parent_domain, desc: "Assigns new domain a parent domain by domain name of the parent. If no parent domain is specied, the ROOT domain is assumed." 30 | def create(name) 31 | create_domains([options.merge(name: name)]) 32 | end 33 | 34 | desc 'delete', 'delete domain' 35 | option :parent_domain, desc: "Parent domain by domain name of the parent. If no parent domain is specied, the ROOT domain is assumed." 36 | def delete(name) 37 | delete_domains([options.merge(name: name)]) 38 | end 39 | 40 | no_commands do 41 | 42 | def create_domains(domains) 43 | puts domains 44 | domains.each do |domain| 45 | say "Creating domain '#{domain['name']}'... " 46 | 47 | if dom = client.list_domains(name: domain["name"], listall: true).first 48 | unless domain["parent_domain"] && dom['parentdomainname'] != domain["parent_domain"] 49 | say "domain '#{domain["name"]}' already exists.", :yellow 50 | next 51 | end 52 | end 53 | 54 | if domain["parent_domain"] 55 | parent = client.list_domains(name: domain["parent_domain"], listall: true).first 56 | unless parent 57 | say "parent domain '#{domain["parent_domain"]}' of domain '#{domain["name"]}' not found.", :yellow 58 | next 59 | end 60 | domain['parentdomain_id'] = parent['id'] 61 | end 62 | 63 | client.create_domain(domain) ? say("OK.", :green) : say("Failed.", :red) 64 | end 65 | end 66 | 67 | def delete_domains(domains) 68 | domains.each do |domain| 69 | print "Deleting domain '#{domain['name']}'..." 70 | if dom = client.list_domains(name: domain["name"], listall: true).first 71 | if domain["parent_domain"] && dom['parentdomainname'] =! domain["parent_domain"] 72 | say "domain '#{domain["name"]}' with same name found, but parent_domain '#{domain["parent_domain"]}' does not match.", :yellow 73 | next 74 | end 75 | client.delete_domain(id: dom['id']) ? say(" OK.", :green) : say(" Failed.", :red) 76 | else 77 | say "domain '#{domain["name"]}' not found.", :yellow 78 | end 79 | end 80 | end 81 | 82 | end 83 | 84 | end 85 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/environment.rb: -------------------------------------------------------------------------------- 1 | class Environment < CloudstackCli::Base 2 | 3 | desc "list", "list cloudstack-cli environments" 4 | def list 5 | config = parse_config_file 6 | table = [%w(Name URL Default)] 7 | table << ['-', config[:url], !config[:default]] if config.key?(:url) 8 | config.each_key do |key| 9 | unless key.class == Symbol 10 | table << [key, config[key][:url], key == config[:default]] 11 | end 12 | end 13 | print_table table 14 | end 15 | 16 | desc "add", "add a new Cloudstack environment" 17 | option :url 18 | option :api_key 19 | option :secret_key 20 | option :default, type: :boolean 21 | def add(env = options[:environment]) 22 | config = {} 23 | unless options[:url] 24 | say "Add a new environment...", :green 25 | if env 26 | say "Environment name: #{env}" 27 | else 28 | env = ask("Environment name:", :magenta) 29 | end 30 | say "What's the URL of your Cloudstack API?", :yellow 31 | say "Example: https://my-cloudstack-service/client/api/", :green 32 | config[:url] = ask("URL:", :magenta) 33 | end 34 | 35 | unless options[:api_key] 36 | config[:api_key] = ask("API Key:", :magenta) 37 | end 38 | 39 | unless options[:secret_key] 40 | config[:secret_key] = ask("Secret Key:", :magenta) 41 | end 42 | 43 | if env 44 | config = {env => config} 45 | config[:default] = env if options[:default] 46 | end 47 | 48 | if File.exist? options[:config_file] 49 | old_config = parse_config_file 50 | if !env || old_config.has_key?(env) 51 | say "This environment already exists!", :red 52 | exit unless yes?("Do you want to override your settings? [y/N]", :yellow) 53 | end 54 | config = old_config.merge(config) 55 | else 56 | newfile = true 57 | config[:default] = env if env 58 | end 59 | 60 | write_config_file(config) 61 | if newfile 62 | say "OK. Created configuration file at #{options[:config_file]}.", :green 63 | else 64 | say "Added environment #{env}", :green 65 | end 66 | end 67 | 68 | desc "delete", "delete a Cloudstack connection" 69 | def delete(env) 70 | config = parse_config_file 71 | if env == '-' 72 | config.delete(:url) 73 | config.delete(:api_key) 74 | config.delete(:secret_key) 75 | # check if the config file is empty, delete it if true 76 | if config.keys.select { |key| !key.is_a? Symbol}.size == 0 77 | exit unless yes?("Do you really want to delete environment #{env}? [y/N]", :yellow) 78 | File.delete(options[:config_file]) 79 | say "OK.", :green 80 | exit 81 | end 82 | elsif config.delete(env) 83 | else 84 | say "Environment #{env} does not exist.", :red 85 | exit 1 86 | end 87 | exit unless yes?("Do you really want to delete environment #{env}? [y/N]", :yellow) 88 | config.delete :default if config[:default] == env 89 | write_config_file(config) 90 | say "OK.", :green 91 | end 92 | 93 | desc "default [ENV]", "show or set the default environment" 94 | def default(env = nil) 95 | config = parse_config_file 96 | 97 | unless env 98 | default_env = config[:default] || '-' 99 | say "The current default environment is \"#{default_env}\"" 100 | exit 0 101 | end 102 | 103 | if env == '-' && config.key?(:url) 104 | config.delete :default 105 | else 106 | unless config.has_key?(env) 107 | say "Environment #{env} does not exist.", :red 108 | exit 1 109 | end 110 | config[:default] = env 111 | end 112 | 113 | write_config_file(config) 114 | say "Default environment set to #{env}." 115 | end 116 | 117 | no_commands do 118 | 119 | def parse_config_file 120 | if File.exist? options[:config_file] 121 | begin 122 | return YAML::load(IO.read(options[:config_file])) 123 | rescue 124 | say "Error loading configuration from file #{options[:config_file]}.", :red 125 | exit 1 126 | end 127 | else 128 | say "Can't load configuration from file #{options[:config_file]}.", :red 129 | exit 1 130 | end 131 | end 132 | 133 | def write_config_file(config) 134 | begin 135 | return File.open(options[:config_file], 'w+') {|f| f.write(config.to_yaml) } 136 | rescue 137 | say "Can't open configuration file #{options[:config_file]} for writing.", :red 138 | exit 1 139 | end 140 | end 141 | 142 | end 143 | 144 | end 145 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/host.rb: -------------------------------------------------------------------------------- 1 | class Host < CloudstackCli::Base 2 | 3 | desc 'list', 'list hosts' 4 | option :zone, desc: "lists hosts by zone" 5 | option :type, desc: "the host type" 6 | option :format, default: "table", 7 | enum: %w(table json yaml) 8 | def list 9 | resolve_zone if options[:zone] 10 | hosts = client.list_hosts(options) 11 | if hosts.size < 1 12 | say "No hosts found." 13 | else 14 | case options[:format].to_sym 15 | when :yaml 16 | puts({hosts: hosts}.to_yaml) 17 | when :json 18 | puts JSON.pretty_generate(hosts: hosts) 19 | else 20 | table = [["Zone", "Type", "Cluster", "Name"]] 21 | hosts.each do |host| 22 | table << [ 23 | host['zonename'], host['type'], host['clustername'], host['name'] 24 | ] 25 | end 26 | print_table table 27 | say "Total number of hosts: #{hosts.size}" 28 | end 29 | end 30 | end 31 | 32 | desc 'show', 'show host details' 33 | def show(name) 34 | unless host = client.list_hosts(name: name).first 35 | say "No host with name '#{name}' found." 36 | else 37 | table = host.map do |key, value| 38 | [ set_color("#{key}:", :yellow), "#{value}" ] 39 | end 40 | print_table table 41 | end 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/ip_address.rb: -------------------------------------------------------------------------------- 1 | class IpAddress < CloudstackCli::Base 2 | 3 | desc "release ID [ID2 ID3]", "release public IP address by ID" 4 | def release(*ids) 5 | ids.each do |id| 6 | say(" OK, released address with ID #{id}", :green) if client.disassociate_ip_address(id: id) 7 | end 8 | end 9 | 10 | desc "assign NETWORK", "assign a public IP address" 11 | option :project 12 | def assign(network) 13 | resolve_project 14 | options[:name] = network 15 | unless network = client.list_networks(options).first 16 | error "Network #{network} not found." 17 | exit 1 18 | end 19 | 20 | if address = client.associate_ip_address(networkid: network["id"]) 21 | say " OK. Assigned IP address:", :green 22 | table = [%w(ID Address Account Zone)] 23 | table << [address["id"], address["ipaddress"], address["account"], address["zonename"]] 24 | print_table table 25 | end 26 | end 27 | 28 | desc "list", "list public IP address" 29 | option :project 30 | option :account 31 | option :listall, type: :boolean, default: true 32 | option :format, default: "table", 33 | enum: %w(table json yaml) 34 | def list 35 | resolve_account 36 | resolve_project 37 | addresses = client.list_public_ip_addresses(options) 38 | if addresses.size < 1 39 | say "No ip addresses found." 40 | else 41 | case options[:format].to_sym 42 | when :yaml 43 | puts({ip_addresses: addresses}.to_yaml) 44 | when :json 45 | puts JSON.pretty_generate(ip_addresses: addresses) 46 | else 47 | table = [%w(ID Address Account Zone)] 48 | addresses.each do |address| 49 | table << [address["id"], address["ipaddress"], address["account"], address["zonename"]] 50 | end 51 | print_table table 52 | say "Total number of addresses: #{addresses.size}" 53 | end 54 | end 55 | end 56 | 57 | end 58 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/iso.rb: -------------------------------------------------------------------------------- 1 | class Iso < CloudstackCli::Base 2 | 3 | desc 'list', "list ISO's" 4 | option :project, desc: 'project name' 5 | option :zone, desc: 'zone name' 6 | option :account, desc: 'account name' 7 | option :type, desc: 'type of ISO', 8 | enum: %w(featured self selfexecutable sharedexecutable executable community all) 9 | option :format, default: "table", 10 | enum: %w(table json yaml) 11 | option :filter, type: :hash, 12 | desc: "filter objects based on arrtibutes: (attr1:regex attr2:regex ...)" 13 | def list 14 | add_filters_to_options("listIsos") if options[:filter] 15 | resolve_project 16 | resolve_zone 17 | resolve_account 18 | options[:isofilter] = options[:type] 19 | options.delete :type 20 | isos = client.list_isos(options) 21 | isos = filter_objects(isos) if options[:filter] 22 | if isos.size < 1 23 | puts "No ISO's found." 24 | else 25 | case options[:format].to_sym 26 | when :yaml 27 | puts({isos: isos}.to_yaml) 28 | when :json 29 | puts JSON.pretty_generate(isos: isos) 30 | else 31 | table = [%w(Name Zone Bootable Public Featured)] 32 | isos.each do |iso| 33 | table << [ 34 | iso['name'], 35 | iso['zonename'], 36 | iso['bootable'], 37 | iso['ispublic'], 38 | iso['isfeatured'] 39 | ] 40 | end 41 | print_table(table) 42 | say "Total number of ISO's: #{isos.size}" 43 | end 44 | end 45 | end 46 | 47 | desc 'attach', "attaches an ISO to a virtual machine" 48 | option :iso, desc: 'ISO file name' 49 | option :project, desc: 'project name' 50 | option :virtual_machine, desc: 'virtual machine name' 51 | option :virtual_machine_id, desc: 'virtual machine id (if no virtual machine name profided)' 52 | def attach 53 | resolve_iso 54 | resolve_project 55 | unless options[:virtual_machine_id] 56 | resolve_virtual_machine 57 | end 58 | options[:id] = options[:iso_id] 59 | client.attach_iso(options.merge(sync: false)) 60 | say " OK", :green 61 | end 62 | 63 | desc 'detach', "detaches any ISO file (if any) currently attached to a virtual machine" 64 | option :project, desc: 'project name' 65 | option :virtual_machine, desc: 'virtual machine name' 66 | option :virtual_machine_id, desc: 'virtual machine id (if no virtual machine name profided)' 67 | def detach 68 | resolve_project 69 | unless options[:virtual_machine_id] 70 | resolve_virtual_machine 71 | end 72 | client.detach_iso(options.merge(sync: true)) 73 | say " OK", :green 74 | end 75 | 76 | end 77 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/job.rb: -------------------------------------------------------------------------------- 1 | class Job < CloudstackCli::Base 2 | 3 | desc 'list', 'list async jobs' 4 | option :format, default: "table", 5 | enum: %w(table json yaml) 6 | def list 7 | jobs = client.list_async_jobs 8 | if jobs.size < 1 9 | say "No jobs found." 10 | else 11 | case options[:format].to_sym 12 | when :yaml 13 | puts({jobs: jobs}.to_yaml) 14 | when :json 15 | puts JSON.pretty_generate(jobs: jobs) 16 | else 17 | table = [%w(Command Created Status ID User-ID)] 18 | jobs.each do |job| 19 | table << [ 20 | job['cmd'].split('.')[-1], 21 | job['created'], 22 | job['jobstatus'], 23 | job['jobid'], 24 | job['userid'] 25 | ] 26 | end 27 | print_table table 28 | end 29 | end 30 | end 31 | 32 | desc 'query ID', 'query async job' 33 | def query(id) 34 | job = client.query_async_job_result(jobid: id) 35 | table = job.map do |key, value| 36 | [ set_color("#{key}:", :yellow), "#{value}" ] 37 | end 38 | print_table table 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/load_balancer.rb: -------------------------------------------------------------------------------- 1 | class LoadBalancer < CloudstackCli::Base 2 | 3 | desc "list", "list load balancer rules" 4 | option :project 5 | option :format, default: "table", 6 | enum: %w(table json yaml) 7 | def list 8 | resolve_project 9 | rules = client.list_load_balancer_rules(options) 10 | if rules.size < 1 11 | puts "No load balancer rules found." 12 | else 13 | case options[:format].to_sym 14 | when :yaml 15 | puts({rules: rules}.to_yaml) 16 | when :json 17 | puts JSON.pretty_generate(rules: rules) 18 | else 19 | table = [%w(Name Public-IP Public-Port Private-Port Algorithm)] 20 | rules.each do |rule| 21 | table << [ 22 | rule['name'], 23 | rule['publicip'], 24 | rule['publicport'], 25 | rule['privateport'], 26 | rule['algorithm'] 27 | ] 28 | end 29 | print_table table 30 | say "Total number of rules: #{rules.count}" 31 | end 32 | end 33 | end 34 | 35 | desc "create NAME", "create load balancer rule" 36 | option :project 37 | option :ip, required: true 38 | option :public_port, required: true 39 | option :private_port 40 | option :algorithm, 41 | enum: %w(source roundrobin leastconn), 42 | default: "roundrobin" 43 | option :open_firewall, type: :boolean 44 | option :cidr_list 45 | option :protocol 46 | def create(name) 47 | resolve_project 48 | ip_options = {ip_address: options[:ip]} 49 | ip_options[:project_id] = options[:project_id] if options[:project_id] 50 | unless ip = client.list_public_ip_addresses(ip_options).first 51 | say "Error: IP #{options[:ip]} not found.", :red 52 | exit 1 53 | end 54 | options[:private_port] = options[:public_port] if options[:private_port] == nil 55 | options[:name] = name 56 | options[:publicip_id] = ip['id'] 57 | say "Create rule #{name}...", :yellow 58 | rule = client.create_load_balancer_rule(options) 59 | say " OK!", :green 60 | end 61 | 62 | desc "add LB-NAME", "assign servers to balancer rule" 63 | option :servers, 64 | required: true, 65 | type: :array, 66 | desc: "server names" 67 | option :project 68 | def add(name) 69 | resolve_project 70 | default_args = options.dup 71 | default_args.delete(:servers) 72 | 73 | servers = options[:servers].map do |server| 74 | client.list_virtual_machines(default_args.merge(name: server)).first 75 | end.compact 76 | 77 | unless servers.size > 0 78 | say "No servers found with the following name(s): #{options[:servers].join(', ')}", :yellow 79 | exit 1 80 | end 81 | 82 | unless rule = client.list_load_balancer_rules(default_args.merge(name: name)).first 83 | say "Error: LB rule with name #{name} not found.", :red 84 | exit 1 85 | end 86 | 87 | say "Add #{servers.map{|s| s['name']}.join(', ')} to rule #{name} ", :yellow 88 | lb = client.assign_to_load_balancer_rule( 89 | { 90 | id: rule['id'], 91 | virtualmachine_ids: servers.map{|s| s['id']}.join(',') 92 | }.merge(default_args) 93 | ) 94 | 95 | if lb['success'] 96 | say " OK.", :green 97 | else 98 | say " Failed.", :red 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/network.rb: -------------------------------------------------------------------------------- 1 | class Network < CloudstackCli::Base 2 | 3 | desc "list", "list networks" 4 | option :project, desc: 'the project name of the network' 5 | option :account, desc: 'the owner of the network' 6 | option :zone, desc: 'the name of the zone the network belongs to' 7 | option :showid, type: :boolean, desc: 'show the network id' 8 | option :showvlan, type: :boolean, desc: 'show the VLAN' 9 | option :filter, type: :hash, 10 | desc: "filter objects based on arrtibutes: (attr1:regex attr2:regex ...)" 11 | option :format, default: "table", 12 | enum: %w(table json yaml) 13 | def list 14 | resolve_zone if options[:zone] 15 | resolve_project 16 | add_filters_to_options("listNetworks") if options[:filter] 17 | networks = client.list_networks(options) 18 | networks = filter_objects(networks) if options[:filter] 19 | if networks.size < 1 20 | puts "No networks found." 21 | else 22 | case options[:format].to_sym 23 | when :yaml 24 | puts({networks: networks}.to_yaml) 25 | when :json 26 | puts JSON.pretty_generate(networks: networks) 27 | else 28 | table = [%w(Name Displaytext Account/Project Zone Domain State Type Offering)] 29 | table[0] << "ID" if options[:showid] 30 | table[0] << "VLAN" if options[:showvlan] 31 | networks.each do |network| 32 | table << [ 33 | network["name"], 34 | network["displaytext"], 35 | network["account"] || network["project"], 36 | network["zonename"], 37 | network["domain"], 38 | network["state"], 39 | network["type"], 40 | network["networkofferingname"] 41 | ] 42 | table[-1] << network["id"] if options[:showid] 43 | table[-1] << network["vlan"] if options[:showvlan] 44 | end 45 | print_table table 46 | say "Total number of networks: #{networks.count}" 47 | end 48 | end 49 | end 50 | 51 | desc "show NAME", "show detailed infos about a network" 52 | option :project 53 | def show(name) 54 | resolve_project 55 | unless network = client.list_networks(options).find {|n| n['name'] == name} 56 | say "Error: No network with name '#{name}' found.", :red 57 | exit 58 | end 59 | table = network.map do |key, value| 60 | [ set_color("#{key}:", :yellow), "#{value}" ] 61 | end 62 | print_table table 63 | end 64 | 65 | desc "restart NAME", "restart network" 66 | option :cleanup, type: :boolean, default: false 67 | option :project 68 | def restart(name) 69 | resolve_project 70 | unless network = client.list_networks(options).find {|n| n['name'] == name} 71 | say "Network with name '#{name}' not found." 72 | exit 1 73 | end 74 | if yes? "Restart network \"#{network['name']}\" (cleanup=#{options[:cleanup]})?" 75 | client.restart_network(id: network['id'], cleanup: options[:cleanup]) 76 | end 77 | end 78 | 79 | desc "delete NAME", "delete network" 80 | option :project 81 | def delete(name) 82 | resolve_project 83 | unless network = client.list_networks(options).find {|n| n['name'] == name} 84 | say "Error: Network with name '#{name}' not found.", :red 85 | exit 1 86 | end 87 | if yes? "Delete network \"#{network['name']}\"?" 88 | client.delete_network(id: network['id']) 89 | end 90 | end 91 | 92 | end 93 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/network_offer.rb: -------------------------------------------------------------------------------- 1 | class NetworkOffer < CloudstackCli::Base 2 | 3 | desc 'list', 'list network offerings' 4 | option :guest_ip_type, enum: %w(isolated shared), 5 | desc: "list network offerings by guest type." 6 | option :format, default: "table", 7 | enum: %w(table json yaml) 8 | def list 9 | offerings = client.list_network_offerings(options) 10 | if offerings.size < 1 11 | puts "No offerings found." 12 | else 13 | case options[:format].to_sym 14 | when :yaml 15 | puts({network_offers: offerings}.to_yaml) 16 | when :json 17 | puts JSON.pretty_generate(network_offers: offerings) 18 | else 19 | table = [%w(Name Display_Text Default? Guest_IP_Type State)] 20 | offerings.each do |offer| 21 | table << [ 22 | offer['name'], 23 | offer['displaytext'], 24 | offer['isdefault'], 25 | offer['guestiptype'], 26 | offer['state'], 27 | ] 28 | end 29 | print_table table 30 | end 31 | end 32 | end 33 | 34 | desc "show NAME", "show detailed infos about a network offering" 35 | def show(name) 36 | unless offer = client.list_network_offerings(name: name).first 37 | say "Error: No network offering with name '#{name}' found.", :red 38 | else 39 | table = offer.map do |key, value| 40 | if key == "service" 41 | [ set_color("services", :yellow), value.map{|s| s["name"]}.join(", ") ] 42 | else 43 | [ set_color("#{key}", :yellow), "#{value}" ] 44 | end 45 | end 46 | print_table table 47 | end 48 | end 49 | 50 | end 51 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/physical_network.rb: -------------------------------------------------------------------------------- 1 | class PhysicalNetwork < CloudstackCli::Base 2 | 3 | desc "list", "list physical networks" 4 | option :project 5 | option :format, default: "table", 6 | enum: %w(table json yaml) 7 | def list 8 | resolve_project 9 | networks = client.list_physical_networks(options) 10 | zones = client.list_zones 11 | if networks.size < 1 12 | puts "No networks found" 13 | else 14 | case options[:format].to_sym 15 | when :yaml 16 | puts({networks: networks}.to_yaml) 17 | when :json 18 | puts JSON.pretty_generate(networks: networks) 19 | else 20 | table = [['Name', 'State', 'Zone', 'ID']] 21 | networks.each do |network| 22 | table << [ 23 | network["name"], 24 | network["state"], 25 | zones.select{|zone| zone['id'] == network["zoneid"]}.first["name"], 26 | network["id"] 27 | ] 28 | end 29 | print_table table 30 | say "Total number of networks: #{networks.count}" 31 | end 32 | end 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/pod.rb: -------------------------------------------------------------------------------- 1 | class Pod < CloudstackCli::Base 2 | 3 | desc 'list', 'list pods' 4 | option :zone 5 | option :format, default: "table", 6 | enum: %w(table json yaml) 7 | def list 8 | resolve_zone 9 | pods = client.list_pods(options) 10 | if pods.size < 1 11 | say "No pods found." 12 | else 13 | case options[:format].to_sym 14 | when :yaml 15 | puts({pods: pods}.to_yaml) 16 | when :json 17 | puts JSON.pretty_generate(pods: pods) 18 | else 19 | table = [["Name", "Start-IP", "End-IP", "Zone"]] 20 | pods.each do |pod| 21 | table << [ 22 | pod['name'], pod['startip'], 23 | pod['endip'], pod['zonename'] 24 | ] 25 | end 26 | print_table table 27 | say "Total number of pods: #{pods.count}" 28 | end 29 | end 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/port_rule.rb: -------------------------------------------------------------------------------- 1 | class PortRule < CloudstackCli::Base 2 | 3 | desc "create VM-NAME", "create portforwarding rules for a given VM" 4 | option :rules, type: :array, 5 | required: true, 6 | desc: "Port Forwarding Rules [public_ip]:port ...", 7 | aliases: '-r' 8 | option :network, required: true, aliases: '-n' 9 | option :project 10 | option :keyword, desc: "list by keyword" 11 | def create(server_name) 12 | resolve_project 13 | unless server = client.list_virtual_machines( 14 | name: server_name, project_id: options[:project_id], listall: true 15 | ).find {|vm| vm["name"] == server_name } 16 | error "Server #{server_name} not found." 17 | exit 1 18 | end 19 | ip_addr = nil 20 | options[:rules].each do |pf_rule| 21 | ip = pf_rule.split(":")[0] 22 | unless ip == '' 23 | unless ip_addr = client.list_public_ip_addresses(ipaddress: ip, project_id: options[:project_id]).first 24 | say "Error: IP #{ip} not found.", :yellow 25 | next 26 | end 27 | else 28 | say "Assign a new IP address ", :yellow 29 | net_id = client.list_networks(project_id: options[:project_id]).find {|n| n['name'] == options[:network]}['id'] 30 | say(" OK", :green) if ip_addr = client.associate_ip_address(networkid: net_id)["ipaddress"] 31 | end 32 | port = pf_rule.split(":")[1] 33 | say "Create port forwarding rule #{ip_addr["ipaddress"]}:#{port} for server #{server_name} ", :yellow 34 | 35 | say(" OK", :green) if client.create_port_forwarding_rule( 36 | ipaddress_id: ip_addr["id"], 37 | public_port: port, 38 | private_port: port, 39 | virtualmachine_id: server["id"], 40 | protocol: "TCP" 41 | ) 42 | end 43 | end 44 | 45 | desc "list", "list portforwarding rules" 46 | option :project 47 | option :format, default: "table", 48 | enum: %w(table json yaml) 49 | def list 50 | resolve_project 51 | rules = client.list_port_forwarding_rules(options) 52 | if rules.size < 1 53 | puts "No rules found." 54 | else 55 | case options[:format].to_sym 56 | when :yaml 57 | puts({rules: rules}.to_yaml) 58 | when :json 59 | puts JSON.pretty_generate(rules: rules) 60 | else 61 | table = [["IP", "Server", "Public-Port", "Private-Port", "Protocol", "State"]] 62 | rules.each do |rule| 63 | table << [ 64 | rule['ipaddress'], 65 | rule['virtualmachinename'], 66 | print_ports(rule, 'public'), 67 | print_ports(rule, 'private'), 68 | rule['protocol'], 69 | rule['state'] 70 | ] 71 | end 72 | print_table table 73 | say "Total number of rules: #{rules.count}" 74 | end 75 | end 76 | end 77 | 78 | no_commands do 79 | def print_ports(rule, type) 80 | if rule["#{type}port"] == rule["#{type}endport"] 81 | return rule["#{type}port"] 82 | else 83 | return rule["#{type}port"] + "-" + rule["#{type}endport"] 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/project.rb: -------------------------------------------------------------------------------- 1 | class Project < CloudstackCli::Base 2 | 3 | desc "show NAME", "show detailed infos about a project" 4 | def show(name) 5 | unless project = client.list_projects(name: name, listall: true).first 6 | say "Error: No project with name '#{name}' found.", :red 7 | else 8 | table = project.map do |key, value| 9 | [ set_color("#{key}", :yellow), "#{value}" ] 10 | end 11 | print_table table 12 | end 13 | end 14 | 15 | desc "list", "list projects" 16 | option :domain, desc: "List only resources belonging to the domain specified" 17 | option :format, default: "table", 18 | enum: %w(table json yaml) 19 | def list 20 | resolve_domain 21 | projects = client.list_projects(options.merge listall: true) 22 | if projects.size < 1 23 | puts "No projects found." 24 | else 25 | case options[:format].to_sym 26 | when :yaml 27 | puts({projects: projects}.to_yaml) 28 | when :json 29 | puts JSON.pretty_generate(projects: projects) 30 | else 31 | table = [%w(Name Displaytext VMs CPU Memory Domain)] 32 | projects.each do |project| 33 | table << [ 34 | project['name'], 35 | project['displaytext'], 36 | project['vmtotal'], 37 | project['cputotal'], 38 | project['memorytotal'] / 1024, 39 | project['domain'] 40 | ] 41 | end 42 | print_table(table) 43 | say "Total number of projects: #{projects.count}" 44 | end 45 | end 46 | end 47 | 48 | desc "list_accounts PROJECT_NAME", "show accounts belonging to a project" 49 | def list_accounts(name) 50 | unless project = client.list_projects(name: name, listall: true).first 51 | say "Error: No project with name '#{name}' found.", :red 52 | else 53 | accounts = client.list_project_accounts(project_id: project['id']) 54 | if accounts.size < 1 55 | say "No project accounts found." 56 | else 57 | table = [%w(Account-Name Account-Type Role Domain)] 58 | accounts.each do |account| 59 | table << [ 60 | account['account'], 61 | Account::TYPES[account['accounttype']], 62 | account['role'], 63 | account['domain'] 64 | ] 65 | end 66 | print_table table 67 | say "Total number of project accounts: #{accounts.size}" 68 | end 69 | end 70 | end 71 | 72 | end 73 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/region.rb: -------------------------------------------------------------------------------- 1 | class Region < CloudstackCli::Base 2 | 3 | desc 'list', 'list regions' 4 | option :format, default: "table", 5 | enum: %w(table json yaml) 6 | def list 7 | regions = client.list_regions 8 | if regions.size < 1 9 | say "No regions found." 10 | else 11 | case options[:format].to_sym 12 | when :yaml 13 | puts({regions: regions}.to_yaml) 14 | when :json 15 | puts JSON.pretty_generate(regions: regions) 16 | else 17 | table = [%w(Name, Endpoint)] 18 | regions.each do |region| 19 | table << [ 20 | region['name'], region['endpoint'] 21 | ] 22 | end 23 | print_table table 24 | say "Total number of regions: #{regions.size}" 25 | end 26 | end 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/resource_limit.rb: -------------------------------------------------------------------------------- 1 | class ResourceLimit < CloudstackCli::Base 2 | RESOURCE_TYPES = { 3 | 0 => {name: "Instances"}, 4 | 1 => {name: "IP Addresses"}, 5 | 2 => {name: "Volumes"}, 6 | 3 => {name: "Snapshots"}, 7 | 4 => {name: "Templates"}, 8 | 5 => {name: "Projects"}, 9 | 6 => {name: "Networks"}, 10 | 7 => {name: "VPC's"}, 11 | 8 => {name: "CPU's"}, 12 | 9 => {name: "Memory", unit: "GB", divider: 1024.0}, 13 | 10 => {name: "Primary Storage", unit: "TB", divider: 1024.0}, 14 | 11 => {name: "Secondary Storage", unit: "TB", divider: 1024.0} 15 | } 16 | 17 | desc "list", "list resource limits" 18 | option :account 19 | option :project 20 | option :type, desc: "specify type, see types for a list of types" 21 | option :format, default: "table", 22 | enum: %w(table json yaml) 23 | def list 24 | resolve_account 25 | resolve_project 26 | limits = client.list_resource_limits(options) 27 | table = [] 28 | header = options[:project] ? ["Project"] : ["Account"] 29 | header += ["Type", "Resource Name", "Max"] 30 | limits.each do |limit| 31 | limit['resourcetype'] = limit['resourcetype'].to_i 32 | table << [ 33 | options[:project] ? limit['project'] : limit['account'], 34 | limit['resourcetype'], 35 | RESOURCE_TYPES[limit['resourcetype']][:name], 36 | resource_to_s(limit, 'max') 37 | ] 38 | end 39 | 40 | case options[:format].to_sym 41 | when :yaml 42 | puts({resource_limits: limits}.to_yaml) 43 | when :json 44 | puts JSON.pretty_generate(resource_limits: limits) 45 | else 46 | table = table.insert(0, header) 47 | print_table table 48 | end 49 | end 50 | 51 | desc "refresh", "refresh resource counts" 52 | option :domain, desc: "refresh resource for a specified domain" 53 | option :account, desc: "refresh resource for a specified account" 54 | option :project, desc: "refresh resource for a specified project" 55 | option :type, desc: "specify type, see types for a list of types" 56 | def refresh 57 | resolve_domain 58 | resolve_account 59 | resolve_project 60 | options[:resource_type] = options[:type] if options[:type] 61 | 62 | unless ['domain_id', 'account', 'project'].any? {|k| options.key?(k)} 63 | say "Error: Please provide domain, account or project.", :red 64 | exit 1 65 | end 66 | 67 | if resource_count = client.update_resource_count(options) 68 | say "Sucessfully refreshed resource limits.", :green 69 | else 70 | say "Error refreshing resource limits.", :red 71 | exit 1 72 | end 73 | end 74 | 75 | desc "update", "update resource counts" 76 | option :domain, desc: "update resource for a specified domain" 77 | option :account, desc: "update resource for a specified account" 78 | option :project, desc: "update resource for a specified project" 79 | option :type, 80 | desc: "specify type, see types for a list of types", 81 | required: true 82 | option :max, 83 | desc: "Maximum resource limit.", 84 | required: true 85 | def update 86 | resolve_domain 87 | resolve_account 88 | resolve_project 89 | options[:resource_type] = options[:type] 90 | 91 | unless ['domain_id', 'account', 'project'].any? {|k| options.key?(k)} 92 | say "Error: Please provide domain, account or project.", :red 93 | exit 1 94 | end 95 | 96 | if resource_count = client.update_resource_limit(options) 97 | say "Sucessfully updated resource limits.", :green 98 | else 99 | say "Error updating resource limits.", :red 100 | exit 1 101 | end 102 | end 103 | 104 | desc "types", "show resource types" 105 | def types 106 | table = [['type', 'name']] 107 | RESOURCE_TYPES.each_pair do |type, data| 108 | table << [type, data[:name]] 109 | end 110 | print_table table 111 | end 112 | 113 | no_commands do 114 | 115 | def resource_to_s(limit, entity) 116 | return '-1 (unlimited)' if limit['max'] == -1 117 | value = RESOURCE_TYPES[limit['resourcetype']][:divider] ? 118 | (limit[entity] / RESOURCE_TYPES[limit['resourcetype']][:divider]).round(1) : 119 | limit[entity] 120 | RESOURCE_TYPES[limit['resourcetype']][:unit] ? 121 | "#{value} #{RESOURCE_TYPES[limit['resourcetype']][:unit]}" : 122 | value.to_s 123 | end 124 | 125 | end 126 | 127 | end 128 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/router.rb: -------------------------------------------------------------------------------- 1 | class Router < CloudstackCli::Base 2 | 3 | desc "list", "list virtual routers" 4 | option :project, desc: "name of the project" 5 | option :account, desc: "name of the account" 6 | option :zone, desc: "name of the zone" 7 | option :state, desc: "the status of the router" 8 | option :redundant_state, desc: "the state of redundant virtual router", 9 | enum: %w(master backup fault unknown) 10 | option :reverse, type: :boolean, default: false, desc: "reverse listing of routers" 11 | option :command, 12 | desc: "command to execute for each router", 13 | enum: %w(START STOP REBOOT STOP_START) 14 | option :concurrency, type: :numeric, default: 10, aliases: '-C', 15 | desc: "number of concurrent commands to execute" 16 | option :format, default: "table", 17 | enum: %w(table json yaml) 18 | option :showid, type: :boolean, desc: "display the router ID" 19 | option :verbose, aliases: '-V', desc: "display additional fields" 20 | option :version, desc: "list virtual router elements by version" 21 | def list 22 | resolve_project 23 | resolve_zone 24 | resolve_account 25 | 26 | routers = client.list_routers(options) 27 | # show all routers unless project or account is set 28 | if !options[:project] && !options[:account] 29 | client.list_projects(listall: true).each do |project| 30 | routers = routers + client.list_routers( 31 | options.merge(projectid: project['id']) 32 | ) 33 | end 34 | end 35 | options[:listall] = true 36 | print_routers(routers, options) 37 | execute_router_commands(options[:command].downcase, routers) if options[:command] 38 | end 39 | 40 | desc "list_from_file FILE", "list virtual routers from file" 41 | option :reverse, type: :boolean, default: false, desc: "reverse listing of routers" 42 | option :command, 43 | desc: "command to execute for each router", 44 | enum: %w(START STOP REBOOT) 45 | option :concurrency, type: :numeric, default: 10, aliases: '-C', 46 | desc: "number of concurrent commands to execute" 47 | option :format, default: "table", 48 | enum: %w(table json yaml) 49 | def list_from_file(file) 50 | routers = parse_file(file)["routers"] 51 | print_routers(routers, options) 52 | execute_router_commands(options[:command].downcase, routers) if options[:command] 53 | end 54 | 55 | desc "stop NAME [NAME2 ..]", "stop virtual router(s)" 56 | option :force, desc: "stop without confirmation", type: :boolean, aliases: '-f' 57 | def stop(*names) 58 | routers = names.map {|name| get_router(name)} 59 | print_routers(routers) 60 | exit unless options[:force] || yes?("\nStop router(s) above? [y/N]:", :magenta) 61 | jobs = routers.map do |router| 62 | {id: client.stop_router({id: router['id']}, {sync: true})['jobid'], name: "Stop router #{router['name']}"} 63 | end 64 | puts 65 | watch_jobs(jobs) 66 | end 67 | 68 | desc "start NAME [NAME2 ..]", "start virtual router(s)" 69 | option :force, desc: "start without confirmation", type: :boolean, aliases: '-f' 70 | def start(*names) 71 | routers = names.map {|name| get_router(name)} 72 | print_routers(routers) 73 | exit unless options[:force] || yes?("\nStart router(s) above? [y/N]:", :magenta) 74 | jobs = routers.map do |router| 75 | {id: client.start_router({id: router['id']}, {sync: true})['jobid'], name: "Start router #{router['name']}"} 76 | end 77 | puts 78 | watch_jobs(jobs) 79 | end 80 | 81 | desc "reboot NAME [NAME2 ..]", "reboot virtual router(s)" 82 | option :force, desc: "reboot without confirmation", type: :boolean, aliases: '-f' 83 | def reboot(*names) 84 | routers = names.map {|name| client.list_routers(name: name).first} 85 | print_routers(routers) 86 | exit unless options[:force] || yes?("\nReboot router(s) above? [y/N]:", :magenta) 87 | jobs = routers.map do |router| 88 | {id: client.reboot_router({id: router['id']}, {sync: true})['jobid'], name: "Reboot router #{router['name']}"} 89 | end 90 | puts 91 | watch_jobs(jobs) 92 | end 93 | 94 | desc "stop_start NAME [NAME2 ..]", "stops and starts virtual router(s)" 95 | option :force, desc: "stop_start without confirmation", type: :boolean, aliases: '-f' 96 | def stop_start(*names) 97 | routers = names.map {|name| get_router(name)} 98 | print_routers(routers) 99 | exit unless options[:force] || yes?("\nRestart router(s) above? [y/N]:", :magenta) 100 | jobs = routers.map do |router| 101 | {id: client.stop_router({id: router['id']}, {sync: true})['jobid'], name: "Stop router #{router['name']}"} 102 | end 103 | puts 104 | watch_jobs(jobs) 105 | 106 | jobs = routers.map do |router| 107 | {id: client.start_router({id: router['id']}, {sync: true})['jobid'], name: "Start router #{router['name']}"} 108 | end 109 | puts 110 | watch_jobs(jobs) 111 | 112 | say "Finished.", :green 113 | end 114 | 115 | desc "destroy NAME [NAME2 ..]", "destroy virtual router(s)" 116 | option :force, desc: "destroy without asking", type: :boolean, aliases: '-f' 117 | def destroy(*names) 118 | routers = names.map {|name| get_router(name)} 119 | print_routers(routers) 120 | exit unless options[:force] || yes?("\nDestroy router(s) above? [y/N]:", :magenta) 121 | jobs = routers.map do |router| 122 | {id: client.destroy_router({id: router['id']}, {sync: true})['jobid'], name: "Destroy router #{router['name']}"} 123 | end 124 | puts 125 | watch_jobs(jobs) 126 | end 127 | 128 | desc "show NAME [NAME2 ..]", "show detailed infos about a virtual router(s)" 129 | option :project 130 | def show(*names) 131 | routers = names.map {|name| get_router(name)} 132 | table = [] 133 | routers.each do |router| 134 | router.each do |key, value| 135 | table << [ set_color("#{key}:", :yellow), "#{value}" ] 136 | end 137 | table << [ "-" * 20 ] unless router == routers[-1] 138 | end 139 | print_table table 140 | end 141 | 142 | no_commands do 143 | 144 | def get_router(name) 145 | unless router = client.list_routers(name: name, listall: true).first 146 | unless router = client.list_routers(name: name, project_id: -1).first 147 | say "Can't find router with name #{name}.", :red 148 | exit 1 149 | end 150 | end 151 | router 152 | end 153 | 154 | def print_routers(routers, options = {}) 155 | if routers.size < 1 156 | say "No routers found." 157 | else 158 | if options[:redundant_state] 159 | routers = filter_by( 160 | routers, 161 | 'redundantstate', 162 | options[:redundant_state].downcase 163 | ) 164 | end 165 | routers.reverse! if options[:reverse] 166 | 167 | options[:format] ||= "table" 168 | case options[:format].to_sym 169 | when :yaml 170 | puts({routers: routers}.to_yaml) 171 | when :json 172 | puts JSON.pretty_generate(routers: routers) 173 | else 174 | table = [%w( 175 | Name Zone Account/Project IP Linklocal-IP Status Version 176 | )] 177 | table[0].unshift('ID') if options[:showid] 178 | if options[:verbose] 179 | table[0].push('Redundant-State', 'Requ-Upgrade', 'Offering') 180 | end 181 | routers.each do |router| 182 | table << [ 183 | router["name"], 184 | router["zonename"], 185 | router["project"] || router["account"], 186 | router["nic"] && router["nic"].first ? router["nic"].first['ipaddress'] : "-", 187 | router["linklocalip"] || "-", 188 | router["state"], 189 | router["version"] || "-" 190 | ] 191 | table[-1].unshift(router["id"]) if options[:showid] 192 | if options[:verbose] 193 | table[-1].push( 194 | print_redundant_state(router), 195 | router["requiresupgrade"] || "-", 196 | router["serviceofferingname"] 197 | ) 198 | end 199 | end 200 | print_table table 201 | puts 202 | say "Total number of routers: #{routers.size}" 203 | end 204 | end 205 | end 206 | 207 | def print_redundant_state(router) 208 | router["isredundantrouter"] == "true" ? router["redundantstate"] : "non-redundant" 209 | end 210 | 211 | def execute_router_commands(command, routers) 212 | unless %w(start stop reboot stop_start).include?(command) 213 | say "\nCommand #{options[:command]} not supported.", :red 214 | exit 1 215 | end 216 | exit unless yes?("\n#{command.capitalize} the router(s) above? [y/N]:", :magenta) 217 | 218 | command.split("_").each do |cmd| 219 | jobs = routers.map do |router| 220 | { 221 | job_id: nil, 222 | args: { id: router["id"] }, 223 | name: "#{cmd.capitalize} router #{router['name']}", 224 | status: -1 225 | } 226 | end 227 | run_background_jobs(jobs, "#{cmd}_router") 228 | end 229 | end 230 | 231 | end 232 | 233 | end 234 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/snapshot.rb: -------------------------------------------------------------------------------- 1 | class Snapshot < CloudstackCli::Base 2 | 3 | desc 'list', 'list snapshots' 4 | option :account, desc: "the account associated with the snapshot" 5 | option :project, desc: "the project associated with the snapshot" 6 | option :domain, desc: "the domain name of the snapshot's account" 7 | option :listall, type: :boolean, default: true, desc: "list all resources the caller has rights on" 8 | option :state, desc: "filter snapshots by state" 9 | option :format, default: "table", 10 | enum: %w(table json yaml) 11 | def list 12 | resolve_account 13 | resolve_project 14 | resolve_domain 15 | snapshots = client.list_snapshots(options) 16 | if snapshots.size < 1 17 | say "No snapshots found." 18 | else 19 | case options[:format].to_sym 20 | when :yaml 21 | puts({snapshots: snapshots}.to_yaml) 22 | when :json 23 | puts JSON.pretty_generate(snapshots: snapshots) 24 | else 25 | table = [%w(Account Name Volume Created Type State)] 26 | snapshots = filter_by(snapshots, :state, options[:state]) if options[:state] 27 | snapshots.each do |snapshot| 28 | table << [ 29 | snapshot['account'], snapshot['name'], snapshot['volumename'], 30 | snapshot['created'], snapshot['snapshottype'], snapshot['state'] 31 | ] 32 | end 33 | print_table table 34 | say "Total number of snapshots: #{snapshots.size}" 35 | end 36 | end 37 | end 38 | 39 | end 40 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/ssh_key_pair.rb: -------------------------------------------------------------------------------- 1 | class SshKeyPair < CloudstackCli::Base 2 | 3 | desc "list", 'list ssh key pairs' 4 | option :listall, type: :boolean, default: true 5 | option :account, desc: "name of the account" 6 | option :project, desc: "name of the project" 7 | option :format, default: "table", 8 | enum: %w(table json yaml) 9 | def list 10 | resolve_account 11 | resolve_project 12 | pairs = client.list_ssh_key_pairs(options) 13 | if pairs.size < 1 14 | say "No ssh key pairs found." 15 | else 16 | case options[:format].to_sym 17 | when :yaml 18 | puts({ssh_key_pairs: pairs}.to_yaml) 19 | when :json 20 | puts JSON.pretty_generate(ssh_key_pairs: pairs) 21 | else 22 | table = [["Name", "Fingerprint"]] 23 | pairs.each do |pair| 24 | table << [pair['name'], pair['fingerprint']] 25 | end 26 | print_table table 27 | end 28 | end 29 | end 30 | 31 | desc 'create NAME', 'create ssh key pair' 32 | option :account, desc: "name of the account" 33 | option :project, desc: "name of the project" 34 | def create(name) 35 | resolve_account 36 | resolve_project 37 | options[:name] = name 38 | pair = client.create_ssh_key_pair(options) 39 | say "Name : #{pair['name']}" 40 | say "Fingerprint : #{pair['fingerprint']}" 41 | say "Privatekey:" 42 | say pair['privatekey'] 43 | end 44 | 45 | desc 'register NAME', 'register ssh key pair' 46 | option :account, desc: "name of the account" 47 | option :project, desc: "name of the project" 48 | option :public_key, aliases: %w(-k), required: true, desc: "path to public_key file" 49 | def register(name) 50 | resolve_account 51 | resolve_project 52 | options[:name] = name 53 | if File.exist?(options[:public_key]) 54 | public_key = IO.read(options[:public_key]) 55 | options[:public_key] = public_key 56 | else 57 | say("Can't open public key #{options[:public_key]}", :red) 58 | exit 1 59 | end 60 | pair = client.register_ssh_key_pair(options) 61 | say "Name : #{pair['name']}" 62 | say "Fingerprint : #{pair['fingerprint']}" 63 | say "Privatekey : #{pair['privatekey']}" 64 | puts 65 | rescue => e 66 | say "Failed to register key: #{e.message}", :red 67 | exit 1 68 | end 69 | 70 | desc 'delete NAME', 'delete ssh key pair' 71 | option :account, desc: "name of the account" 72 | option :project, desc: "name of the project" 73 | option :force, aliases: '-f', desc: "delete without asking" 74 | def delete(name) 75 | resolve_account 76 | resolve_project 77 | options[:name] = name 78 | if options[:force] || yes?("Delete ssh key pair #{name}?", :yellow) 79 | if client.delete_ssh_key_pair(options)['success'] == "true" 80 | say("OK", :green) 81 | else 82 | say("Failed", :red) 83 | exit 1 84 | end 85 | end 86 | end 87 | 88 | desc 'reset_vm_keys', 'resets the SSH Key for virtual machine (the virtual machine must be in a "Stopped" state)' 89 | option :keypair, aliases: %w(-k), desc: "name of keypair", required: true 90 | option :virtual_machine, aliases: %w(-m), desc: "name of virtual machine", required: true 91 | option :account, desc: "name of the account" 92 | option :project, desc: "name of the project" 93 | def reset_vm_keys 94 | resolve_account 95 | resolve_project 96 | 97 | unless virtual_machine = client.list_virtual_machines({name: options[:virtual_machine], list_all: true}.merge options).first 98 | puts "No virtual machine found." 99 | else 100 | unless virtual_machine['state'].downcase == "stopped" 101 | say "ERROR: Virtual machine must be in stopped state.", :red 102 | exit 1 103 | end 104 | unless options[:force] || yes?("Reset ssh key for VM #{options[:virtual_machine]}? (y/N)", :yellow) 105 | exit 106 | end 107 | client.reset_ssh_key_for_virtual_machine(options.merge(id: virtual_machine['id'])) 108 | say "OK", :green 109 | end 110 | end 111 | 112 | end 113 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/stack.rb: -------------------------------------------------------------------------------- 1 | class Stack < CloudstackCli::Base 2 | 3 | desc "create STACKFILE", "create a stack of VM's" 4 | option :limit, type: :array, aliases: '-l', 5 | desc: "Limit on specific server names." 6 | option :skip_forwarding_rules, default: false, 7 | type: :boolean, aliases: '-s', 8 | desc: "Skip creation of port forwarding rules." 9 | option :concurrency, type: :numeric, default: 10, aliases: '-C', 10 | desc: "number of concurrent commands to execute" 11 | option :assumeyes, type: :boolean, default: false, aliases: '-y', 12 | desc: "answer yes for all questions" 13 | def create(stackfile) 14 | stack = parse_file(stackfile) 15 | project_id = find_project_by_name(stack["project"]) 16 | 17 | say "Create stack #{stack["name"]}...", :green 18 | jobs = [] 19 | stack["servers"].each do |instance| 20 | string_to_array(instance["name"]).each do |name| 21 | if !options[:limit] || options[:limit].include?(name) 22 | if server = client.list_virtual_machines( 23 | name: name, project_id: project_id, listall: true 24 | ).find {|vm| vm["name"] == name } 25 | say "VM #{name} (#{server["state"]}) already exists.", :yellow 26 | jobs << { 27 | id: 0, 28 | name: "Create VM #{name}", 29 | status: 3 30 | } 31 | else 32 | options.merge!({ 33 | displayname: instance["decription"], 34 | zone: instance["zone"] || stack["zone"], 35 | project: stack["project"], 36 | template: instance["template"], 37 | iso: instance["iso"] , 38 | offering: instance["offering"], 39 | networks: load_string_or_array(instance["networks"]), 40 | ip_network_list: instance["ip_network_list"], 41 | disk_offering: instance["disk_offering"], 42 | size: instance["disk_size"], 43 | group: instance["group"] || stack["group"], 44 | keypair: instance["keypair"] || stack["keypair"], 45 | ip_address: instance["ip_address"] 46 | }) 47 | vm_options_to_params 48 | jobs << { 49 | job_id: nil, 50 | args: options.merge(name: name), 51 | name: "Create VM #{name}", 52 | status: -1 53 | } 54 | end 55 | end 56 | end 57 | end 58 | 59 | if jobs.count{|job| job[:status] < 1 } > 0 60 | run_background_jobs(jobs, "deploy_virtual_machine") 61 | end 62 | 63 | # count jobs with status 1 => Completed 64 | successful_jobs = jobs.count {|job| job[:status] == 1 } 65 | unless successful_jobs == 0 || options[:skip_forwarding_rules] 66 | say "Check for port forwarding rules...", :green 67 | pjobs = [] 68 | jobs.select{|job| job[:status] == 1}.each do |job| 69 | vm = job[:result]["virtualmachine"] 70 | vm_def = find_vm_in_stack(vm["name"], stack) 71 | if port_rules = string_to_array(vm_def["port_rules"]) 72 | create_port_rules(vm, port_rules, false).each_with_index do |job_id, index| 73 | job_name = "Create port forwarding rules (#{port_rules[index]}) for VM #{vm["name"]}" 74 | pjobs << {id: job_id, name: job_name} 75 | end 76 | end 77 | end 78 | watch_jobs(pjobs) 79 | pjobs.each do |job| 80 | if job[:result] 81 | result = job[:result]["portforwardingrule"] 82 | puts "Created port forwarding rule #{result['ipaddress']}:#{result['publicport']} => #{result['privateport']} for VM #{result['virtualmachinename']}" 83 | end 84 | end 85 | end 86 | say "Finished.", :green 87 | 88 | if successful_jobs > 0 89 | if options[:assumeyes] || yes?("Display password(s) for VM(s)? [y/N]:", :yellow) 90 | pw_table = [%w(VM Password)] 91 | jobs.select {|job| job[:status] == 1 && job[:result] }.each do |job| 92 | if result = job[:result]["virtualmachine"] 93 | pw_table << ["#{result["name"]}:", result["password"] || "n/a"] 94 | end 95 | end 96 | print_table(pw_table) if pw_table.size > 0 97 | end 98 | end 99 | end 100 | 101 | desc "destroy STACKFILE", "destroy a stack of VMs" 102 | option :force, 103 | desc: "destroy without asking", 104 | type: :boolean, 105 | default: false, 106 | aliases: '-f' 107 | option :expunge, 108 | desc: "expunge VMs immediately", 109 | type: :boolean, 110 | default: false, 111 | aliases: '-E' 112 | option :limit, type: :array, aliases: '-l', 113 | desc: "Limit on specific server names." 114 | def destroy(stackfile) 115 | stack = parse_file(stackfile) 116 | project_id = find_project_by_name(stack["project"]) 117 | servers = [] 118 | stack["servers"].each do |server| 119 | string_to_array(server["name"]).each do |name| 120 | if !options[:limit] || options[:limit].include?(name) 121 | servers << name 122 | end 123 | end 124 | end 125 | 126 | if servers.size == 0 127 | say "No servers in stack selected.", :yellow 128 | exit 129 | end 130 | 131 | if options[:force] || 132 | yes?("Destroy #{'and expunge ' if options[:expunge]}the following VM(s)? #{servers.join(', ')} [y/N]:", :yellow) 133 | jobs = [] 134 | servers.each do |name| 135 | if server = client.list_virtual_machines( 136 | name: name, project_id: project_id, listall: true 137 | ).find {|vm| vm["name"] == name } 138 | jobs << { 139 | id: client.destroy_virtual_machine( 140 | { id: server['id'], expunge: options[:expunge] }, 141 | { sync: true } 142 | )['jobid'], 143 | name: "Destroy VM #{name}" 144 | } 145 | end 146 | end 147 | watch_jobs(jobs) 148 | say "Finished.", :green 149 | end 150 | end 151 | 152 | no_commands do 153 | def find_project_by_name(name) 154 | if name 155 | unless project = client.list_projects(name: name, listall: true).first 156 | say "Error: Project '#{name}' not found.", :red 157 | exit 1 158 | end 159 | project_id = project['id'] 160 | else 161 | project_id = nil 162 | end 163 | project_id 164 | end 165 | 166 | def load_string_or_array(item) 167 | return nil if item == nil 168 | item.is_a?(Array) ? item : [item] 169 | end 170 | 171 | def string_to_array(string) 172 | string ? string.gsub(', ', ',').split(',') : nil 173 | end 174 | 175 | def find_vm_in_stack(name, stack) 176 | stack["servers"].each do |server| 177 | if string_to_array(server["name"]).find{|n| n == name } 178 | return server 179 | end 180 | end 181 | end 182 | 183 | end # no_commands 184 | 185 | end 186 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/storage_pool.rb: -------------------------------------------------------------------------------- 1 | class StoragePool < CloudstackCli::Base 2 | 3 | desc 'list', 'list storage_pools' 4 | option :zone, desc: "zone name for the storage pool" 5 | option :name, desc: "name of the storage pool" 6 | option :keyword, desc: "list by keyword" 7 | option :state, desc: "filter by state (Up, Maintenance)" 8 | option :format, default: "table", 9 | enum: %w(table json yaml) 10 | def list 11 | resolve_zone 12 | storage_pools = client.list_storage_pools(options) 13 | if storage_pools.size < 1 14 | say "No storage pools found." 15 | else 16 | case options[:format].to_sym 17 | when :yaml 18 | puts({storage_pools: storage_pools}.to_yaml) 19 | when :json 20 | puts JSON.pretty_generate(storage_pools: storage_pools) 21 | else 22 | storage_pools = filter_by(storage_pools, "state", options[:state]) if options[:state] 23 | table = [%w(Name Pod State Zone)] 24 | table[0] << "Size [GB]" 25 | table[0] << "Used [GB]" 26 | table[0] << "Used [%]" 27 | table[0] << "Alocated [GB]" 28 | table[0] << "Alocated [%]" 29 | table[0] << "Type" 30 | storage_pools.each do |storage_pool| 31 | total = storage_pool['disksizetotal'] / 1024**3 32 | used = (storage_pool['disksizeused'] / 1024**3) rescue 0 33 | allocated = (storage_pool['disksizeallocated'] / 1024**3) rescue 0 34 | table << [ 35 | storage_pool['name'], storage_pool['podname'], 36 | storage_pool['state'], storage_pool['zonename'], 37 | total, used, (100.0 / total * used).round(0), 38 | allocated, (100.0 / total * allocated).round(0), 39 | storage_pool['type'] 40 | ] 41 | end 42 | print_table table 43 | say "Total number of storage_pools: #{storage_pools.size}" 44 | end 45 | end 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/system_vm.rb: -------------------------------------------------------------------------------- 1 | class SystemVm < CloudstackCli::Base 2 | 3 | desc 'list', 'list system VMs' 4 | option :zone, desc: "the name of the availability zone" 5 | option :state, desc: "state of the system VM" 6 | option :type, desc: "the system VM type.", 7 | enum: %w(consoleproxy secondarystoragevm) 8 | option :format, default: "table", 9 | enum: %w(table json yaml) 10 | def list 11 | resolve_zone 12 | vms = client.list_system_vms(options) 13 | if vms.size < 1 14 | say "No system VM's found." 15 | else 16 | case options[:format].to_sym 17 | when :yaml 18 | puts({system_vms: vms}.to_yaml) 19 | when :json 20 | puts JSON.pretty_generate(system_vms: vms) 21 | else 22 | table = [%w(Name Zone State Type)] 23 | vms.each do |vm| 24 | table << [ 25 | vm['name'], vm['zonename'], vm['state'], vm['systemvmtype'] 26 | ] 27 | end 28 | print_table table 29 | say "Total number of system VM's: #{vms.size}" 30 | end 31 | end 32 | end 33 | 34 | desc 'show NAME', 'show system VM' 35 | def show(name) 36 | unless vm = client.list_system_vms(name: name).first 37 | say "No system vm with name #{name} found." 38 | else 39 | table = vm.map do |key, value| 40 | [ set_color("#{key}:", :yellow), "#{value}" ] 41 | end 42 | print_table table 43 | end 44 | end 45 | 46 | desc "start NAME", "start a system VM" 47 | def start(name) 48 | unless vm = client.list_system_vms(name: name).first 49 | say "No system vm with name #{name} found." 50 | else 51 | say("Starting system VM #{name}", :magenta) 52 | client.start_system_vm(id: vm['id']) 53 | say " OK.", :green 54 | end 55 | end 56 | 57 | desc "stop NAME", "stop a system VM" 58 | def stop(name) 59 | unless vm = client.list_system_vms(name: name).first 60 | say "No system vm with name #{name} found." 61 | else 62 | exit unless options[:force] || yes?("Stop system VM #{name}? [y/N]:", :magenta) 63 | client.stop_system_vm(id: vm['id']) 64 | say " OK.", :green 65 | end 66 | end 67 | 68 | desc "reboot NAME", "reboot a system VM" 69 | def reboot(name) 70 | unless vm = client.list_system_vms(name: name).first 71 | say "No system vm with name #{name} found." 72 | else 73 | exit unless options[:force] || yes?("Reboot system VM #{name}? [y/N]:", :magenta) 74 | client.reboot_system_vm(id: vm['id']) 75 | say " OK.", :green 76 | end 77 | end 78 | 79 | end 80 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/template.rb: -------------------------------------------------------------------------------- 1 | class Template < CloudstackCli::Base 2 | 3 | desc 'list', 'list templates' 4 | option :project 5 | option :zone 6 | option :type, 7 | enum: %w(featured self self-executable executable community all), 8 | default: "featured" 9 | option :format, default: "table", 10 | enum: %w(table json yaml) 11 | def list(type='featured') 12 | resolve_project 13 | resolve_zone 14 | options[:template_filter] = options[:type] 15 | templates = client.list_templates(options) 16 | if templates.size < 1 17 | puts "No templates found." 18 | else 19 | case options[:format].to_sym 20 | when :yaml 21 | puts({templates: templates}.to_yaml) 22 | when :json 23 | puts JSON.pretty_generate(templates: templates) 24 | else 25 | table = [%w(Name Created Zone Featured Public Format)] 26 | templates.each do |template| 27 | table << [ 28 | template['name'], 29 | (Time.parse(template['created']).strftime("%F") rescue "-"), 30 | template['zonename'], 31 | template['isfeatured'], 32 | template['ispublic'], 33 | template['format'] 34 | ] 35 | end 36 | print_table(table) 37 | say "Total number of templates: #{templates.size}" 38 | end 39 | end 40 | end 41 | 42 | end 43 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/user.rb: -------------------------------------------------------------------------------- 1 | class User < CloudstackCli::Base 2 | 3 | desc 'list', 'list users' 4 | option :listall, type: :boolean, default: true 5 | option :account 6 | option :format, default: "table", 7 | enum: %w(table json yaml) 8 | def list 9 | resolve_account 10 | users = client.list_users(options) 11 | if users.size < 1 12 | say "No users found." 13 | else 14 | case options[:format].to_sym 15 | when :yaml 16 | puts({users: users}.to_yaml) 17 | when :json 18 | puts JSON.pretty_generate(users: users) 19 | else 20 | table = [%w(Account Type Name Username Email State Domain)] 21 | users.each do |user| 22 | table << [ 23 | user['account'], 24 | Account::TYPES[user['accounttype']], 25 | "#{user['firstname']} #{user['lastname']}", 26 | user['username'], user['email'], 27 | user['state'], user['domain'] 28 | ] 29 | end 30 | print_table table 31 | say "Total number of users: #{users.size}" 32 | end 33 | end 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/virtual_machine.rb: -------------------------------------------------------------------------------- 1 | require 'thread' 2 | 3 | class VirtualMachine < CloudstackCli::Base 4 | 5 | desc "list", "list virtual machines" 6 | option :account, desc: "name of the account" 7 | option :project, desc: "name of the project" 8 | option :zone, desc: "the name of the availability zone" 9 | option :state, desc: "state of the virtual machine" 10 | option :host, desc: "the name of the hypervisor host the VM belong to" 11 | option :filter, type: :hash, 12 | desc: "filter objects based on arrtibutes: (attr1:regex attr2:regex ...)" 13 | option :listall, desc: "list all virtual machines", type: :boolean, default: true 14 | option :command, 15 | desc: "command to execute for the given virtual machines", 16 | enum: %w(START STOP REBOOT) 17 | option :concurrency, type: :numeric, default: 10, aliases: '-C', 18 | desc: "number of concurrent commands to execute" 19 | option :format, default: "table", 20 | enum: %w(table json yaml) 21 | option :force, desc: "execute command without confirmation", type: :boolean, aliases: '-f' 22 | def list 23 | add_filters_to_options("listVirtualMachines") if options[:filter] 24 | resolve_account 25 | resolve_project 26 | resolve_zone 27 | resolve_host 28 | resolve_iso 29 | if options[:command] 30 | command = options[:command].downcase 31 | options.delete(:command) 32 | end 33 | virtual_machines = client.list_virtual_machines(options) 34 | virtual_machines = filter_objects(virtual_machines) if options[:filter] 35 | if virtual_machines.size < 1 36 | puts "No virtual machines found." 37 | else 38 | print_virtual_machines(virtual_machines) 39 | execute_virtual_machines_commands(command, virtual_machines, options) if command 40 | end 41 | end 42 | 43 | desc "list_from_file FILE", "list virtual machines from file" 44 | option :command, 45 | desc: "command to execute for the given virtual machines", 46 | enum: %w(START STOP REBOOT) 47 | option :concurrency, type: :numeric, default: 10, aliases: '-C', 48 | desc: "number of concurrent command to execute" 49 | option :format, default: :table, enum: %w(table json yaml) 50 | option :force, desc: "execute command without confirmation", type: :boolean, aliases: '-f' 51 | def list_from_file(file) 52 | virtual_machines = parse_file(file)["virtual_machines"] 53 | if virtual_machines.size < 1 54 | puts "No virtual machines found." 55 | else 56 | print_virtual_machines(virtual_machines) 57 | execute_virtual_machines_commands( 58 | options[:command].downcase, 59 | virtual_machines, 60 | options 61 | ) if options[:command] 62 | end 63 | end 64 | 65 | desc "show NAME", "show detailed infos about a virtual machine" 66 | option :project 67 | def show(name) 68 | resolve_project 69 | options[:virtual_machine] = name 70 | virtual_machine = resolve_virtual_machine(true) 71 | table = virtual_machine.map do |key, value| 72 | [ set_color("#{key}:", :yellow), "#{value}" ] 73 | end 74 | print_table table 75 | end 76 | 77 | desc "create NAME [NAME2 ...]", "create virtual machine(s)" 78 | option :template, aliases: '-t', desc: "name of the template" 79 | option :iso, desc: "name of the iso template" 80 | option :offering, aliases: '-o', required: true, desc: "computing offering name" 81 | option :zone, aliases: '-z', required: true, desc: "availability zone name" 82 | option :networks, aliases: '-n', type: :array, desc: "network names" 83 | option :project, aliases: '-p', desc: "project name" 84 | option :port_rules, type: :array, 85 | default: [], 86 | desc: "Port Forwarding Rules [public_ip]:port ..." 87 | option :disk_offering, desc: "disk offering (data disk for template, root disk for iso)" 88 | option :disk_size, desc: "disk size in GB" 89 | option :hypervisor, desc: "only used for iso deployments, default: vmware" 90 | option :keypair, desc: "the name of the ssh keypair to use" 91 | option :group, desc: "group name" 92 | option :account, desc: "account name" 93 | option :ip_address, desc: "the ip address for default vm's network" 94 | option :ip_network_list, desc: "ip_network_list (net1:ip net2:ip...)", type: :array 95 | option :user_data, 96 | desc: "optional binary data that can be sent to the virtual machine upon a successful deployment." 97 | option :concurrency, type: :numeric, default: 10, aliases: '-C', 98 | desc: "number of concurrent commands to execute" 99 | option :assumeyes, type: :boolean, default: false, aliases: '-y', 100 | desc: "answer yes for all questions" 101 | def create(*names) 102 | if names.size == 0 103 | say "Please provide at least one virtual machine name.", :yellow 104 | exit 1 105 | end 106 | 107 | if options[:ip_network_list] 108 | options[:ip_network_list] = array_to_network_list(options[:ip_network_list]) 109 | end 110 | 111 | vm_options_to_params 112 | say "Start deploying virtual machine#{"s" if names.size > 1}...", :green 113 | jobs = names.map do |name| 114 | if virtual_machine = find_vm_by_name(name) 115 | say "virtual machine #{name} (#{virtual_machine["state"]}) already exists.", :yellow 116 | job = {name: "Create virtual machine #{name}", status: 3} 117 | else 118 | job = { 119 | args: options.merge(name: name), 120 | name: "Create VM #{name}", 121 | status: -1 122 | } 123 | end 124 | job 125 | end 126 | 127 | if jobs.count{|job| job[:status] < 1 } > 0 128 | run_background_jobs(jobs, "deploy_virtual_machine") 129 | end 130 | 131 | successful_jobs = jobs.count {|job| job[:status] == 1 } 132 | if options[:port_rules].size > 0 && successful_jobs > 0 133 | say "Create port forwarding rules...", :green 134 | pjobs = [] 135 | jobs.select{|job| job[:status] == 1}.each do |job| 136 | vm = job[:result]["virtualmachine"] 137 | create_port_rules(vm, options[:port_rules], false).each_with_index do |job_id, index| 138 | pjobs << { 139 | id: job_id, 140 | name: "Create port forwarding rule #{options[:port_rules][index]} for VM #{vm['name']}" 141 | } 142 | end 143 | end 144 | watch_jobs(pjobs) 145 | end 146 | say "Finished.", :green 147 | 148 | if successful_jobs > 0 149 | if options[:assumeyes] || yes?("Display password(s) for VM(s)? [y/N]:", :yellow) 150 | pw_table = [%w(VM Password)] 151 | jobs.select {|job| job[:status] == 1 && job[:result] }.each do |job| 152 | if result = job[:result]["virtualmachine"] 153 | pw_table << ["#{result["name"]}:", result["password"] || "n/a"] 154 | end 155 | end 156 | print_table(pw_table) if pw_table.size > 0 157 | end 158 | end 159 | end 160 | 161 | desc "destroy NAME [NAME2 ..]", "destroy virtual machine(s)" 162 | option :project 163 | option :force, desc: "destroy without confirmation", type: :boolean, aliases: '-f' 164 | option :expunge, desc: "expunge virtual machine immediately", type: :boolean, default: false, aliases: '-E' 165 | def destroy(*names) 166 | if names.size == 0 167 | say "Please provide at least one virtual machine name.", :yellow 168 | exit 1 169 | end 170 | resolve_project 171 | names.each do |name| 172 | unless virtual_machine = find_vm_by_name(name) 173 | say "Virtual machine #{name} not found.", :red 174 | else 175 | action = options[:expunge] ? "Expunge" : "Destroy" 176 | ask = "#{action} #{virtual_machine['name']} (#{virtual_machine['state']})? [y/N]:" 177 | if options[:force] || yes?(ask, :yellow) 178 | say "destroying #{name} " 179 | client.destroy_virtual_machine( 180 | id: virtual_machine["id"], 181 | expunge: options[:expunge] 182 | ) 183 | puts 184 | end 185 | end 186 | end 187 | end 188 | 189 | desc "create_interactive", "interactive creation of a virtual machine with network access" 190 | def create_interactive 191 | bootstrap_server_interactive 192 | end 193 | 194 | desc "stop NAME", "stop a virtual machine" 195 | option :project 196 | option :force 197 | def stop(name) 198 | resolve_project 199 | unless virtual_machine = find_vm_by_name(name) 200 | say "Virtual machine #{name} not found.", :red 201 | exit 1 202 | end 203 | exit unless options[:force] || 204 | yes?("Stop virtual machine #{virtual_machine['name']}? [y/N]:", :magenta) 205 | client.stop_virtual_machine(id: virtual_machine['id']) 206 | puts 207 | end 208 | 209 | desc "start NAME", "start a virtual_machine" 210 | option :project 211 | def start(name) 212 | resolve_project 213 | unless virtual_machine = find_vm_by_name(name) 214 | say "Virtual machine #{name} not found.", :red 215 | exit 1 216 | end 217 | say("Starting virtual machine #{virtual_machine['name']}", :magenta) 218 | client.start_virtual_machine(id: virtual_machine['id']) 219 | puts 220 | end 221 | 222 | desc "reboot NAME", "reboot a virtual machine" 223 | option :project 224 | option :force 225 | def reboot(name) 226 | resolve_project 227 | unless virtual_machine = find_vm_by_name(name) 228 | say "Virtual machine #{name} not found.", :red 229 | exit 1 230 | end 231 | exit unless options[:force] || yes?("Reboot virtual_machine #{virtual_machine["name"]}? [y/N]:", :magenta) 232 | client.reboot_virtual_machine(id: virtual_machine['id']) 233 | puts 234 | end 235 | 236 | desc "update NAME", "update a virtual machine" 237 | option :project, 238 | desc: "project of virtual machine (used for vm lookup, can't be updated)" 239 | option :force, type: :boolean, 240 | desc: "update w/o asking for confirmation" 241 | option :display_name, 242 | desc: "user generated name" 243 | option :display_vm, 244 | desc: "an optional field, whether to the display the vm to the end user or not" 245 | option :group, 246 | desc: "group of the virtual machine" 247 | option :instance_name, 248 | desc: "instance name of the user vm" 249 | option :name, 250 | desc: "new host name of the vm" 251 | option :ostype_id, 252 | desc: "the ID of the OS type that best represents this VM" 253 | option :details, type: :hash, 254 | desc: "details in key/value pairs." 255 | option :is_dynamically_scalable, enum: %w(true false), 256 | desc: "true if VM contains XS/VMWare tools inorder to support dynamic scaling of VM cpu/memory" 257 | option :ha_enable, enum: %w(true false), 258 | desc: "true if high-availability is enabled for the virtual machine, false otherwise" 259 | option :user_data, 260 | desc: "optional binary data that can be sent to the virtual machine upon a successful deployment." 261 | def update(name) 262 | resolve_project 263 | unless vm = find_vm_by_name(name) 264 | say "Virtual machine #{name} not found.", :red 265 | exit 1 266 | end 267 | unless vm["state"].downcase == "stopped" 268 | say "Virtual machine #{name} (#{vm["state"]}) must be in a stopped state.", :red 269 | exit 1 270 | end 271 | unless options[:force] || yes?("Update virtual_machine #{name}? [y/N]:", :magenta) 272 | exit 273 | end 274 | if options[:user_data] 275 | # base64 encode user_data 276 | options[:user_data] = [options[:user_data]].pack("m") 277 | end 278 | vm = client.update_virtual_machine(options.merge(id: vm['id'])) 279 | say "Virtual machine \"#{name}\" has been updated:", :green 280 | 281 | table = vm.select do |k, _| 282 | options.find {|k2, _| k2.gsub('_', '') == k } 283 | end.map do |key, value| 284 | [ set_color("#{key}:", :yellow), set_color("#{value}", :red) ] 285 | end 286 | print_table table 287 | end 288 | 289 | no_commands do 290 | 291 | def find_vm_by_name(name) 292 | client.list_virtual_machines( 293 | name: options[:virtual_machine], 294 | listall: true, 295 | project_id: options[:project_id] 296 | ).find {|vm| vm["name"] == name } 297 | end 298 | 299 | def print_virtual_machines(virtual_machines) 300 | case options[:format].to_sym 301 | when :yaml 302 | puts({virtual_machines: virtual_machines}.to_yaml) 303 | when :json 304 | puts JSON.pretty_generate(virtual_machines: virtual_machines) 305 | else 306 | with_i_name = virtual_machines.first['instancename'] 307 | with_h_name = virtual_machines.first['hostname'] 308 | table = [["Name", "State", "Offering", "Zone", options[:project_id] ? "Project" : "Account", "IP's"]] 309 | table.first.insert(1, "Instance-Name") if with_i_name 310 | table.first.insert(-1, "Host-Name") if with_h_name 311 | virtual_machines.each do |virtual_machine| 312 | table << [ 313 | virtual_machine['name'], 314 | virtual_machine['state'], 315 | virtual_machine['serviceofferingname'], 316 | virtual_machine['zonename'], 317 | options[:project_id] ? virtual_machine['project'] : virtual_machine['account'], 318 | virtual_machine['nic'].map { |nic| nic['ipaddress']}.join(' ') 319 | ] 320 | table.last.insert(1, virtual_machine['instancename']) if with_i_name 321 | table.last.insert(-1, virtual_machine['hostname']) if with_h_name 322 | end 323 | print_table table 324 | say "Total number of virtual machines: #{virtual_machines.count}" 325 | end 326 | end 327 | 328 | def execute_virtual_machines_commands(command, virtual_machines, options = {}) 329 | unless %w(start stop reboot).include?(command) 330 | say "\nCommand #{options[:command]} not supported.", :red 331 | exit 1 332 | end 333 | exit unless options[:force] || 334 | yes?("\n#{command.capitalize} the virtual machine(s) above? [y/N]:", :magenta) 335 | 336 | jobs = virtual_machines.map do |vm| 337 | { 338 | job_id: nil, 339 | args: { id: vm["id"] }, 340 | name: "#{command.capitalize} virtual machine #{vm['name']}", 341 | status: -1 342 | } 343 | end 344 | 345 | run_background_jobs(jobs, "#{command}_virtual_machine") 346 | end 347 | 348 | def array_to_network_list(arr) 349 | arr.each.map do |item| 350 | name = item.split(':')[0] 351 | ip = item.split(':')[1] 352 | {"name" => name, "ip" => ip} 353 | end 354 | end 355 | 356 | end # no_commands 357 | 358 | end 359 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/volume.rb: -------------------------------------------------------------------------------- 1 | class Volume < CloudstackCli::Base 2 | 3 | desc "list", "list volumes" 4 | option :project, desc: 'list resources by project' 5 | option :account, desc: 'list resources by account' 6 | option :zone, desc: "the name of the availability zone" 7 | option :keyword, desc: 'list by keyword' 8 | option :name, desc: 'name of the disk volume' 9 | option :type, desc: 'type of disk volume (ROOT or DATADISK)' 10 | option :filter, type: :hash, 11 | desc: "filter objects based on arrtibutes: (attr1:regex attr2:regex ...)" 12 | option :format, default: "table", 13 | enum: %w(table json yaml) 14 | def list 15 | resolve_project 16 | resolve_account 17 | resolve_zone 18 | add_filters_to_options("listVolumes") if options[:filter] 19 | volumes = client.list_volumes(options) 20 | volumes = filter_objects(volumes) if options[:filter] 21 | if volumes.size < 1 22 | say "No volumes found." 23 | else 24 | case options[:format].to_sym 25 | when :yaml 26 | puts({volumes: volumes}.to_yaml) 27 | when :json 28 | puts JSON.pretty_generate(volumes: volumes) 29 | else 30 | table = [%w(Name Type Size VM Storage Offeringname Zone Status)] 31 | table.first << 'Project' if options[:project] 32 | volumes.each do |volume| 33 | table << [ 34 | volume['name'], volume['type'], 35 | (volume['size'] / 1024**3).to_s + 'GB', 36 | volume['vmname'], 37 | volume['storage'], 38 | volume['diskofferingname'], 39 | volume['zonename'], 40 | volume['state'] 41 | ] 42 | table.last << volume['project'] if options[:project] 43 | end 44 | print_table(table) 45 | say "Total number of volumes: #{volumes.size}" 46 | end 47 | end 48 | end 49 | 50 | desc "show NAME", "show volume details" 51 | option :project, desc: 'project of volume' 52 | def show(name) 53 | resolve_project 54 | options[:listall] = true 55 | options[:name] = name 56 | volumes = client.list_volumes(options) 57 | if volumes.size < 1 58 | say "No volume with name \"#{name}\" found." 59 | else 60 | volume = volumes.first 61 | table = volume.map do |key, value| 62 | [ set_color("#{key}:", :yellow), "#{value}" ] 63 | end 64 | print_table table 65 | end 66 | end 67 | 68 | desc "create NAME", "create volume" 69 | option :project, 70 | desc: "project of volume" 71 | option :disk_offering, 72 | desc: "disk offering for the disk volume. Either disk_offering or snapshot must be passed in" 73 | option :snapshot, 74 | desc: "snapshot for the disk volume. Either disk_offering or snapshot must be passed in" 75 | option :virtual_machine, 76 | desc: "Used together with the snapshot option: VM to which the volume gets attached after creation." 77 | option :zone, 78 | desc: "name of the availability zone" 79 | option :size, 80 | desc: "size of the volume in GB" 81 | def create(name) 82 | options[:name] = name 83 | resolve_project 84 | resolve_zone 85 | resolve_disk_offering 86 | resolve_snapshot 87 | resolve_virtual_machine 88 | 89 | if !options[:disk_offering_id] && !options[:snapshot_id] 90 | say "Either disk_offering or snapshot must be passed in.", :yellow 91 | exit 1 92 | elsif options[:disk_offering_id] && !options[:zone_id] 93 | say "Zone is required when deploying with disk-offering.", :yellow 94 | exit 1 95 | end 96 | 97 | say "Creating volume #{name} " 98 | job = client.create_volume(options).merge(sync: true) 99 | say " OK.", :green 100 | 101 | # attach the new volume if a vm is profided and not a sapshot 102 | if options[:virtual_machine] && options[:snapshot] == nil 103 | sleep 2 104 | say "Attach volume #{name} to VM #{options[:virtual_machine]} " 105 | client.attach_volume( 106 | id: job['volume']['id'], 107 | virtualmachineid: options[:virtual_machine_id], 108 | sync: true 109 | ) 110 | say " OK.", :green 111 | end 112 | end 113 | 114 | desc "attach NAME", "attach volume to VM" 115 | option :project, desc: 'project of volume' 116 | option :virtual_machine, desc: 'virtual machine of volume', required: true 117 | def attach(name) 118 | resolve_project 119 | resolve_virtual_machine 120 | 121 | volume = client.list_volumes( 122 | name: name, 123 | listall: true, 124 | project_id: options[:project_id] 125 | ).first 126 | 127 | if !volume 128 | say "Error: Volume #{name} not found.", :red 129 | exit 1 130 | elsif volume.has_key?("virtualmachineid") 131 | say "Error: Volume #{name} already attached to VM #{volume["vmname"]}.", :red 132 | exit 1 133 | end 134 | 135 | say "Attach volume #{name} to VM #{options[:virtual_machine]} " 136 | client.attach_volume( 137 | id: volume['id'], 138 | virtualmachineid: options[:virtual_machine_id] 139 | ) 140 | say " OK.", :green 141 | end 142 | 143 | desc "detach NAME", "detach volume from VM" 144 | option :project, desc: 'project of volume' 145 | option :force 146 | def detach(name) 147 | resolve_project 148 | 149 | volume = client.list_volumes( 150 | name: name, 151 | listall: true, 152 | project_id: options[:project_id] 153 | ).first 154 | 155 | if !volume 156 | say "Error: Volume #{name} not found.", :red 157 | exit 1 158 | elsif !volume.has_key?("virtualmachineid") 159 | say "Error: Volume #{name} currently not attached to any VM.", :red 160 | exit 1 161 | end 162 | exit unless options[:force] || 163 | yes?("Detach volume #{name} from virtual_machine #{volume["vmname"]}? [y/N]:", :magenta) 164 | say "Detach volume #{name} from VM #{volume["vmname"]} " 165 | client.detach_volume id: volume['id'] 166 | say " OK.", :green 167 | end 168 | 169 | desc "delete NAME", "delete volume to VM" 170 | option :project, desc: 'project of volume' 171 | def delete(name) 172 | resolve_project 173 | 174 | volume = client.list_volumes( 175 | name: name, 176 | listall: true, 177 | project_id: options[:project_id] 178 | ).first 179 | 180 | if !volume 181 | say "Error: Volume #{name} not found.", :red 182 | exit 1 183 | elsif volume.has_key?("virtualmachineid") 184 | say "Error: Volume #{name} must be detached before deletion.", :red 185 | exit 1 186 | end 187 | 188 | say "Delete volume #{name} " 189 | client.delete_volume id: volume['id'] 190 | say " OK.", :green 191 | end 192 | 193 | end 194 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/commands/zone.rb: -------------------------------------------------------------------------------- 1 | class Zone < CloudstackCli::Base 2 | 3 | desc "list", "list zones" 4 | option :format, default: "table", 5 | enum: %w(table json yaml) 6 | def list 7 | zones = client.list_zones 8 | if zones.size < 1 9 | puts "No projects found" 10 | else 11 | case options[:format].to_sym 12 | when :yaml 13 | puts({zones: zones}.to_yaml) 14 | when :json 15 | puts JSON.pretty_generate(zones: zones) 16 | else 17 | table = [%w(Name Network-Type Description)] 18 | zones.each do |zone| 19 | table << [ 20 | zone['name'], 21 | zone['networktype'], 22 | zone['description'] 23 | ] 24 | end 25 | print_table(table) 26 | end 27 | end 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/helper.rb: -------------------------------------------------------------------------------- 1 | module CloudstackCli 2 | module Helper 3 | def print_options(options, attr = 'name') 4 | options.to_enum.with_index(1).each do |option, i| 5 | puts "#{i}: #{option[attr]}" 6 | end 7 | end 8 | 9 | def ask_number(question) 10 | number = ask(question).to_i - 1 11 | number < 0 ? 0 : number 12 | end 13 | 14 | ASYNC_STATES = { 15 | -1 => "waiting", 16 | 0 => "running", 17 | 1 => "completed", 18 | 2 => "error", 19 | 3 => "aborted" 20 | } 21 | 22 | def watch_jobs(jobs) 23 | chars = %w(| / - \\) 24 | call = 0 25 | opts = {t_start: Time.now} 26 | jobs = update_job_status(jobs) 27 | while jobs.count{|job| job[:status].to_i < 1 } > 0 do 28 | if call.modulo(40) == 0 29 | t = Thread.new { jobs = update_job_status(jobs) } 30 | while t.alive? 31 | chars = print_job_status(jobs, chars, 32 | call == 0 ? opts.merge(no_clear: true) : opts 33 | ) 34 | call += 1 35 | end 36 | t.join 37 | else 38 | chars = print_job_status(jobs, chars, 39 | call == 0 ? opts.merge(no_clear: true) : opts 40 | ) 41 | call += 1 42 | end 43 | end 44 | print_job_status(jobs, chars, 45 | call == 0 ? opts.merge(no_clear: true) : opts 46 | ) 47 | end 48 | 49 | def update_job_status(jobs) 50 | jobs.each do |job| 51 | job[:status] = 0 unless job[:status] 52 | if job[:status] == 0 53 | result = client.query_async_job_result(job_id: job[:id]) 54 | job[:status] = result["jobstatus"] 55 | # add result information for terminated jobs 56 | if job[:status].between?(1, 2) 57 | job[:result] = result["jobresult"] 58 | end 59 | end 60 | end 61 | jobs 62 | end 63 | 64 | def run_background_jobs(jobs, command) 65 | view_thread = Thread.new do 66 | chars = %w(| / - \\) 67 | call = 0 68 | opts = {t_start: Time.now} 69 | 70 | while jobs.count{|job| job[:status] < 1 } > 0 do 71 | if call.modulo(40) == 0 72 | t = Thread.new { jobs = update_jobs(jobs, command) } 73 | while t.alive? 74 | chars = print_job_status(jobs, chars, 75 | call == 0 ? opts.merge(no_clear: true) : opts 76 | ) 77 | call += 1 78 | end 79 | t.join 80 | else 81 | chars = print_job_status(jobs, chars, 82 | call == 0 ? opts.merge(no_clear: true) : opts 83 | ) 84 | call += 1 85 | end 86 | end 87 | print_job_status(jobs, chars, 88 | call == 0 ? opts.merge(no_clear: true) : opts 89 | ) 90 | end 91 | view_thread.join 92 | end 93 | 94 | def update_jobs(jobs, command) 95 | # update running job status 96 | threads = jobs.select{|job| job[:status] == 0 }.map do |job| 97 | Thread.new do 98 | result = client.query_async_job_result(job_id: job[:job_id]) 99 | job[:status] = result['jobstatus'] 100 | if job[:status].between?(1, 2) 101 | job[:result] = result["jobresult"] 102 | end 103 | end 104 | end 105 | threads.each(&:join) 106 | 107 | # launch new jobs if required and possible 108 | launch_capacity = (options[:concurrency] ||= 10) - jobs.count{|job| job[:status] == 0 } 109 | threads = [] 110 | jobs.select{|job| job[:status] == -1 }.each do |job| 111 | if launch_capacity > 0 112 | threads << Thread.new do 113 | job[:job_id] = client.send( 114 | command, job[:args], { sync: true } 115 | )['jobid'] 116 | job[:status] = 0 117 | end 118 | launch_capacity -= 1 119 | end 120 | end 121 | threads.each(&:join) 122 | jobs 123 | end 124 | 125 | def print_job_status(jobs, spinner, opts = {t_start: Time.now}) 126 | print ("\r" + "\e[A\e[K" * (jobs.size + 1)) unless opts[:no_clear] 127 | jobs.each_with_index do |job, i| 128 | print "#{job[:name]} : job #{ASYNC_STATES[job[:status]]} " 129 | puts job[:status] == 0 ? spinner.first : "" 130 | end 131 | t_elapsed = opts[:t_start] ? (Time.now - opts[:t_start]).round(1) : 0 132 | completed = jobs.count{|j| j[:status] == 1 } 133 | say "Completed: #{completed} of #{jobs.size} (#{t_elapsed}s)", :magenta 134 | sleep opts[:sleeptime] || 0.1 135 | spinner.push spinner.shift 136 | spinner 137 | end 138 | 139 | def bootstrap_server(args = {}) 140 | if args[:project] && project = find_project(args[:project]) 141 | project_id = project["id"] 142 | project_name = project['name'] 143 | end 144 | 145 | if args[:name] 146 | args['displayname'] = args[:name] 147 | name = args[:name] 148 | elsif args[:displayname] 149 | name = args[:displayname] 150 | end 151 | 152 | unless server = client.list_virtual_machines(name: args[:name], project_id: project_id).first 153 | say "Create VM #{name}...", :yellow 154 | server = client.deploy_virtual_machine(args) 155 | puts 156 | say "VM #{name} has been created.", :green 157 | else 158 | say "VM #{name} already exists (#{server["state"]}).", :yellow 159 | end 160 | 161 | if args[:port_rules] && args[:port_rules].size > 0 162 | create_port_rules(server, args[:port_rules]) 163 | end 164 | server 165 | end 166 | 167 | def create_server(args = {}) 168 | if args[:project] && project = find_project(args[:project]) 169 | project_id = project["id"] 170 | project_name = project['name'] 171 | end 172 | unless server = client.list_virtual_machines(name: args[:name], project_id: project_id) 173 | server = client.deploy_virtual_machine(args) 174 | end 175 | server 176 | end 177 | 178 | def create_port_rules(server, port_rules, async = true) 179 | frontendip_id = nil 180 | jobs = [] 181 | client.verbose = async 182 | project_id = server['projectid'] || nil 183 | port_rules.each do |pf_rule| 184 | pf_rule = pf_rule_to_object(pf_rule) 185 | if pf_rule[:ipaddress] 186 | pub_ip = client.list_public_ip_addresses( 187 | network_id: get_server_default_nic(server)["networkid"], 188 | project_id: project_id, 189 | ipaddress: pf_rule[:ipaddress] 190 | ) 191 | ip_addr = pub_ip.find { |addr| addr['ipaddress'] == pf_rule[:ipaddress]} if pub_ip 192 | if ip_addr 193 | frontendip = ip_addr['id'] 194 | else 195 | say "Error: IP #{pf_rule[:ipaddress]} not found.", :red 196 | next 197 | end 198 | end 199 | 200 | # check if there is already an existing rule 201 | rules = client.list_port_forwarding_rules( 202 | networkid: get_server_default_nic(server)["networkid"], 203 | ipaddressid: frontendip_id, 204 | projectid: project_id 205 | ) 206 | existing_pf_rules = rules.find do |rule| 207 | # remember matching address for additional rules 208 | frontendip_id = rule['ipaddressid'] if rule['virtualmachineid'] == server['id'] 209 | 210 | rule['virtualmachineid'] == server['id'] && 211 | rule['publicport'] == pf_rule[:publicport] && 212 | rule['privateport'] == pf_rule[:privateport] && 213 | rule['protocol'] == pf_rule[:protocol] 214 | end 215 | 216 | if existing_pf_rules 217 | say "Port forwarding rule on port #{pf_rule[:privateport]} for VM #{server["name"]} already exists.", :yellow 218 | else 219 | unless frontendip_id 220 | frontendip_id = client.associate_ip_address( 221 | network_id: get_server_default_nic(server)["networkid"], 222 | project_id: project_id 223 | )['ipaddress']['id'] 224 | end 225 | args = pf_rule.merge({ 226 | ipaddressid: frontendip_id, 227 | virtualmachineid: server["id"] 228 | }) 229 | if async 230 | say "Create port forwarding rule #{pf_rule[:ipaddress]}:#{port} for VM #{server["name"]}.", :yellow 231 | client.create_port_forwarding_rule(args) 232 | return 233 | else 234 | jobs << client.create_port_forwarding_rule(args, {sync: true})['jobid'] 235 | end 236 | end 237 | end 238 | jobs 239 | end 240 | 241 | def bootstrap_server_interactive 242 | zones = client.list_zones 243 | if zones.size > 1 244 | say "Select a availability zone:", :yellow 245 | print_options(zones) 246 | zone = ask_number("Zone Nr.: ") 247 | else 248 | zone = 0 249 | end 250 | 251 | projects = client.list_projects 252 | project_id = nil 253 | if projects.size > 0 254 | if yes?("Do you want to deploy your VM within a project? (y/N)") && projects.size > 0 255 | say "Select a project", :yellow 256 | print_options(projects) 257 | project = ask_number("Project Nr.: ") 258 | project_id = projects[project]['id'] rescue nil 259 | end 260 | end 261 | 262 | say "Please provide a name for the new VM", :yellow 263 | say "(spaces or special characters are NOT allowed)" 264 | server_name = ask("Server name: ") 265 | 266 | server_offerings = client.list_service_offerings 267 | say "Select a computing offering:", :yellow 268 | print_options(server_offerings) 269 | service_offering = ask_number("Offering Nr.: ") 270 | 271 | templates = client.list_templates(project_id: project_id, zone_id: zones[zone]["id"], template_filter: "executable") 272 | say "Select a template:", :yellow 273 | print_options(templates) 274 | template = ask_number("Template Nr.: ") 275 | 276 | networks = client.list_networks(project_id: project_id, zone_id: zones[zone]["id"]) 277 | if networks.size > 1 278 | say "Select a network:", :yellow 279 | print_options(networks) 280 | network = ask_number("Network Nr.: ") 281 | else 282 | network = 0 283 | end 284 | 285 | say "You entered the following configuration:", :yellow 286 | table = [["Zone", zones[zone]["name"]]] 287 | table << ["VM Name", server_name] 288 | table << ["Template", templates[template]["name"]] 289 | table << ["Offering", server_offerings[service_offering]["name"]] 290 | table << ["Network", networks[network]["name"]] 291 | table << ["Project", projects[project]["name"]] if project 292 | print_table table 293 | 294 | if yes? "Do you want to deploy this VM? (y/N)" 295 | bootstrap_server( 296 | name: server_name, 297 | zone_id: zones[zone]["id"], 298 | template_id: templates[template]["id"], 299 | serviceoffering_id: server_offerings[service_offering]["id"], 300 | network_ids: network ? networks[network]["id"] : nil, 301 | project_id: project_id 302 | ) 303 | end 304 | end 305 | 306 | def get_server_default_nic(server) 307 | server['nic'].each do |nic| 308 | return nic if nic['isdefault'] 309 | end 310 | end 311 | 312 | def pf_rule_to_object(pf_rule) 313 | pf_rule = pf_rule.split(":") 314 | { 315 | ipaddress: (pf_rule[0] == '' ? nil : pf_rule[0]), 316 | privateport: pf_rule[1], 317 | publicport: (pf_rule[2] || pf_rule[1]), 318 | protocol: (pf_rule[3] || 'tcp').downcase 319 | } 320 | end 321 | 322 | end 323 | end 324 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/option_resolver.rb: -------------------------------------------------------------------------------- 1 | module CloudstackCli 2 | module OptionResolver 3 | 4 | def vm_options_to_params 5 | resolve_zone 6 | resolve_project 7 | resolve_compute_offering 8 | resolve_template 9 | resolve_disk_offering 10 | resolve_iso_for_vm_deployment if options[:iso] 11 | options[:size] = options[:disk_size] if options[:disk_size] 12 | unless options[:template_id] 13 | say "Error: Template or ISO is required.", :red 14 | exit 1 15 | end 16 | if options[:ip_network_list] 17 | resolve_ip_network_list 18 | else 19 | resolve_networks 20 | end 21 | options 22 | end 23 | 24 | def resolve_zone 25 | if options[:zone] 26 | zones = client.list_zones 27 | zone = zones.find {|z| z['name'] == options[:zone] } 28 | if !zone 29 | msg = options[:zone] ? "Zone '#{options[:zone]}' is invalid." : "No zone found." 30 | say "Error: #{msg}", :red 31 | exit 1 32 | end 33 | options[:zone_id] = zone['id'] 34 | end 35 | options 36 | end 37 | 38 | def resolve_domain 39 | if options[:domain] 40 | if domain = client.list_domains(name: options[:domain]).first 41 | options[:domain_id] = domain['id'] 42 | else 43 | say "Error: Domain #{options[:domain]} not found.", :red 44 | exit 1 45 | end 46 | end 47 | options 48 | end 49 | 50 | def resolve_project 51 | if options[:project] 52 | if %w(ALL -1).include? options[:project] 53 | options[:project_id] = "-1" 54 | elsif project = client.list_projects(name: options[:project], listall: true).first 55 | options[:project_id] = project['id'] 56 | else 57 | say "Error: Project #{options[:project]} not found.", :red 58 | exit 1 59 | end 60 | end 61 | options 62 | end 63 | 64 | def resolve_account 65 | if options[:account] 66 | if account = client.list_accounts(name: options[:account], listall: true).first 67 | options[:account_id] = account['id'] 68 | options[:domain_id] = account['domainid'] 69 | else 70 | say "Error: Account #{options[:account]} not found.", :red 71 | exit 1 72 | end 73 | end 74 | options 75 | end 76 | 77 | def resolve_networks 78 | networks = [] 79 | available_networks = network = client.list_networks( 80 | zone_id: options[:zone_id], 81 | project_id: options[:project_id] 82 | ) 83 | if options[:networks] 84 | options[:networks].each do |name| 85 | unless network = available_networks.find { |n| n['name'] == name } 86 | say "Error: Network '#{name}' not found.", :red 87 | exit 1 88 | end 89 | networks << network['id'] rescue nil 90 | end 91 | end 92 | networks.compact! 93 | if networks.empty? 94 | unless default_network = client.list_networks(project_id: options[:project_id]).first 95 | say "Error: No default network found.", :red 96 | exit 1 97 | end 98 | networks << available_networks.first['id'] rescue nil 99 | end 100 | options[:network_ids] = networks.join(',') 101 | options 102 | end 103 | 104 | def resolve_ip_network_list 105 | network_list = [] 106 | available_networks = network = client.list_networks( 107 | zone_id: options[:zone_id], 108 | project_id: options[:project_id] 109 | ) 110 | if options[:ip_network_list] 111 | options[:ip_network_list].each do |item| 112 | unless network = available_networks.find { |n| n['name'] == item["name"] } 113 | say "Error: Network '#{item["name"]}' not found.", :red 114 | exit 1 115 | end 116 | item.delete("name") 117 | network_list << {networkid: network["id"]}.merge(item) 118 | end 119 | end 120 | network_list.compact! 121 | if network_list.empty? 122 | say "Error: IP network list can't be empty.", :red 123 | exit 1 124 | end 125 | options[:ip_to_network_list] = network_list 126 | [:network_ids, :ip_address].each { |k| options.delete(k) } 127 | options 128 | end 129 | 130 | def resolve_iso 131 | if options[:iso] 132 | iso = false 133 | %w(self featured community).each do |iso_filter| 134 | iso = client.list_isos( 135 | name: options[:iso], 136 | project_id: options[:project_id], 137 | isofilter: iso_filter 138 | ).first 139 | break if iso 140 | end 141 | if iso 142 | options[:iso_id] = iso["id"] 143 | else 144 | say "Error: Iso '#{options[:iso]}' is invalid.", :red 145 | exit 1 146 | end 147 | end 148 | options 149 | end 150 | 151 | def resolve_iso_for_vm_deployment 152 | unless options[:disk_offering_id] 153 | say "Error: a disk offering is required when using iso.", :red 154 | exit 1 155 | end 156 | resolve_iso 157 | options[:template_id] = options[:iso_id] 158 | options['hypervisor'] = (options[:hypervisor] || 'vmware') 159 | options 160 | end 161 | 162 | def resolve_template 163 | if options[:template] 164 | if template = client.list_templates( 165 | name: options[:template], 166 | template_filter: "executable", 167 | project_id: options[:project_id] 168 | ).first 169 | options[:template_id] = template['id'] 170 | else 171 | say "Error: Template #{options[:template]} not found.", :red 172 | exit 1 173 | end 174 | end 175 | options 176 | end 177 | 178 | def resolve_compute_offering 179 | if offering = client.list_service_offerings(name: options[:offering]).first 180 | options[:service_offering_id] = offering['id'] 181 | else 182 | say "Error: Offering #{options[:offering]} not found.", :red 183 | exit 1 184 | end 185 | options 186 | end 187 | 188 | def resolve_disk_offering 189 | if options[:disk_offering] 190 | unless disk_offering = client.list_disk_offerings(name: options[:disk_offering]).first 191 | say "Error: Disk offering '#{options[:disk_offering]}' not found.", :red 192 | exit 1 193 | end 194 | options[:disk_offering_id] = disk_offering['id'] 195 | end 196 | options 197 | end 198 | 199 | def resolve_virtual_machine(return_vm = false) 200 | if options[:virtual_machine] 201 | unless vm = client.list_virtual_machines( 202 | name: options[:virtual_machine], 203 | listall: true, 204 | project_id: options[:project_id] 205 | ).find {|vm| vm["name"] == options[:virtual_machine] } 206 | say "Error: VM '#{options[:virtual_machine]}' not found.", :red 207 | exit 1 208 | end 209 | 210 | if return_vm 211 | return vm 212 | else 213 | options[:virtual_machine_id] = vm["id"] 214 | end 215 | end 216 | options 217 | end 218 | 219 | def resolve_snapshot 220 | if options[:snapshot] 221 | args = { name: options[:snapshot], listall: true } 222 | args[:project_id] = options[:project_id] 223 | unless snapshot = client.list_snapshots(args).first 224 | say "Error: Snapshot '#{options[:snapshot]}' not found.", :red 225 | exit 1 226 | end 227 | options[:snapshot_id] = snapshot['id'] 228 | end 229 | options 230 | end 231 | 232 | def resolve_host(type = "routing") 233 | if options[:host] 234 | args = { name: options[:host], type: type, listall: true } 235 | unless host = client.list_hosts(args).first 236 | say "Error: Host '#{options[:host]}' not found.", :red 237 | exit 1 238 | end 239 | options[:host_id] = host['id'] 240 | end 241 | options 242 | end 243 | 244 | def resolve_cluster 245 | if options[:cluster] 246 | unless cluster = client.list_clusters(name: options[:cluster]).first 247 | say "Error: Cluster '#{options[:cluster]}' not found.", :red 248 | exit 1 249 | end 250 | options[:cluster_id] = cluster['id'] 251 | end 252 | options 253 | end 254 | 255 | end 256 | end 257 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/thor_patch.rb: -------------------------------------------------------------------------------- 1 | # Fixes thor help for subcommands 2 | # https://github.com/erikhuda/thor/pull/330 3 | 4 | class Thor 5 | class << self 6 | attr_accessor :parent_class 7 | 8 | def basename2(subcommand = false) 9 | bn = parent_class && parent_class.basename2 10 | bn2 = basename 11 | ns = self.namespace.split(':').last 12 | bn ? (subcommand ? bn : "#{bn} #{ns}") : bn2 13 | end 14 | 15 | def banner(command, namespace = nil, subcommand = false) 16 | "#{basename2(subcommand)} #{command.formatted_usage(self, $thor_runner, subcommand)}" 17 | end 18 | 19 | alias :old_subcommand :subcommand 20 | 21 | def subcommand(subcommand, subcommand_class) 22 | subcommand_class.parent_class = self 23 | old_subcommand(subcommand, subcommand_class) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/cloudstack-cli/version.rb: -------------------------------------------------------------------------------- 1 | module CloudstackCli 2 | VERSION = "1.6.10" 3 | end 4 | -------------------------------------------------------------------------------- /spec/cloudstack-cli/commands/template_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "cloudstack-cli" 3 | 4 | describe Template do 5 | 6 | it "should list templates" do 7 | out, err = capture_io do 8 | CloudstackCli::Cli.start [ 9 | "template", 10 | "list", 11 | "--zone=#{ZONE}" 12 | ] 13 | end 14 | err.must_equal "" 15 | out.must_include TEMPLATE 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /spec/cloudstack-cli/commands/virtual_machine_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "cloudstack-cli" 3 | 4 | describe VirtualMachine do 5 | 6 | #TODO: test-network must be created beforehand 7 | 8 | it "should support all CRUD actions" do 9 | vmname = "testvm1" 10 | 11 | # CREATE 12 | out, err = capture_io do 13 | CloudstackCli::Cli.start [ 14 | "vm", 15 | "create", 16 | vmname, 17 | "--zone=#{ZONE}", 18 | "--template=#{TEMPLATE}", 19 | "--offering=#{OFFERING_S}", 20 | "--networks=test-network", 21 | "--port-rules=:80", 22 | "--assumeyes" 23 | ] 24 | end 25 | err.must_equal "" 26 | 27 | # READ - LIST 28 | out, err = capture_io{ CloudstackCli::Cli.start [ 29 | "vm", 30 | "list" 31 | ]} 32 | err.must_equal "" 33 | out.must_match( 34 | /.*(#{vmname}).*/ 35 | ) 36 | 37 | # READ - SHOW 38 | out, err = capture_io{ CloudstackCli::Cli.start [ 39 | "vm", 40 | "show", 41 | vmname 42 | ]} 43 | err.must_equal "" 44 | out.must_match( 45 | /.*(#{vmname}).*/ 46 | ) 47 | 48 | # UPDATE - STOP 49 | out, err = capture_io{ CloudstackCli::Cli.start [ 50 | "vm", 51 | "stop", 52 | vmname, 53 | "--force" 54 | ]} 55 | err.must_equal "" 56 | 57 | # UPDATE - UPDATE ;-) 58 | new_vmname = "testvm11" 59 | out, err = capture_io{ CloudstackCli::Cli.start [ 60 | "vm", 61 | "update", 62 | vmname, 63 | "--name=#{new_vmname}", 64 | "--force" 65 | ]} 66 | err.must_equal "" 67 | 68 | # UPDATE - START 69 | out, err = capture_io{ CloudstackCli::Cli.start [ 70 | "vm", 71 | "start", 72 | new_vmname 73 | ]} 74 | err.must_equal "" 75 | 76 | # UPDATE - REBOOT 77 | out, err = capture_io{ CloudstackCli::Cli.start [ 78 | "vm", 79 | "reboot", 80 | new_vmname, 81 | "--force" 82 | ]} 83 | err.must_equal "" 84 | 85 | # DELETE 86 | out, err = capture_io{ CloudstackCli::Cli.start [ 87 | "vm", 88 | "destroy", 89 | new_vmname, 90 | "--expunge", 91 | "--force" 92 | ]} 93 | err.must_equal "" 94 | 95 | end 96 | 97 | end 98 | -------------------------------------------------------------------------------- /spec/fixtures/stack.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "web_stack_a" 3 | description: "Web Application Stack" 4 | version: "1.0" 5 | zone: "Sandbox-simulator" 6 | group: "my_web_stack" 7 | servers: 8 | - name: "web-01, web-02" 9 | description: "Web nodes" 10 | template: "CentOS 5.3(64-bit) no GUI (Simulator)" 11 | offering: "Small Instance" 12 | networks: "test-network" 13 | port_rules: ":80, :443" 14 | - name: "db-01" 15 | description: "PostgreSQL Master" 16 | template: "CentOS 5.3(64-bit) no GUI (Simulator)" 17 | offering: "Medium Instance" 18 | ip_network_list: 19 | - name: "test-network" 20 | ip: 10.1.1.11 21 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "cloudstack-cli" 2 | 3 | require "minitest/spec" 4 | require "minitest/autorun" 5 | require "minitest/pride" 6 | 7 | # make the config file setup available to all specs 8 | ZONE = "Sandbox-simulator" 9 | TEMPLATE = "CentOS 5.6 (64-bit) no GUI (Simulator)" 10 | OFFERING_S = "Small Instance" 11 | OFFERING_M = "Medium Instance" 12 | --------------------------------------------------------------------------------