morpheus-cli 4.2.8 → 4.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/lib/morpheus/api.rb +1 -1
  4. data/lib/morpheus/api/activity_interface.rb +9 -0
  5. data/lib/morpheus/api/api_client.rb +83 -27
  6. data/lib/morpheus/api/apps_interface.rb +21 -0
  7. data/lib/morpheus/api/dashboard_interface.rb +5 -21
  8. data/lib/morpheus/api/instances_interface.rb +3 -10
  9. data/lib/morpheus/api/invoice_line_items_interface.rb +14 -0
  10. data/lib/morpheus/api/invoices_interface.rb +7 -12
  11. data/lib/morpheus/api/library_layouts_interface.rb +8 -0
  12. data/lib/morpheus/api/ping_interface.rb +20 -0
  13. data/lib/morpheus/api/projects_interface.rb +33 -0
  14. data/lib/morpheus/api/setup_interface.rb +19 -36
  15. data/lib/morpheus/api/user_settings_interface.rb +0 -6
  16. data/lib/morpheus/api/whoami_interface.rb +4 -8
  17. data/lib/morpheus/benchmarking.rb +16 -26
  18. data/lib/morpheus/cli.rb +10 -5
  19. data/lib/morpheus/cli/access_token_command.rb +5 -8
  20. data/lib/morpheus/cli/activity_command.rb +146 -0
  21. data/lib/morpheus/cli/apps.rb +312 -121
  22. data/lib/morpheus/cli/archives_command.rb +1 -1
  23. data/lib/morpheus/cli/auth_command.rb +4 -11
  24. data/lib/morpheus/cli/blueprints_command.rb +196 -137
  25. data/lib/morpheus/cli/change_password_command.rb +1 -1
  26. data/lib/morpheus/cli/cli_command.rb +225 -72
  27. data/lib/morpheus/cli/cli_registry.rb +2 -2
  28. data/lib/morpheus/cli/cloud_datastores_command.rb +1 -1
  29. data/lib/morpheus/cli/clouds.rb +5 -20
  30. data/lib/morpheus/cli/clusters.rb +4 -28
  31. data/lib/morpheus/cli/commands/standard/alias_command.rb +2 -9
  32. data/lib/morpheus/cli/commands/standard/benchmark_command.rb +2 -0
  33. data/lib/morpheus/cli/commands/standard/curl_command.rb +2 -3
  34. data/lib/morpheus/cli/commands/standard/history_command.rb +3 -6
  35. data/lib/morpheus/cli/commands/standard/man_command.rb +10 -7
  36. data/lib/morpheus/cli/commands/standard/ssl_verification_command.rb +10 -9
  37. data/lib/morpheus/cli/containers_command.rb +3 -3
  38. data/lib/morpheus/cli/credentials.rb +13 -16
  39. data/lib/morpheus/cli/error_handler.rb +18 -12
  40. data/lib/morpheus/cli/errors.rb +45 -0
  41. data/lib/morpheus/cli/execute_schedules_command.rb +1 -1
  42. data/lib/morpheus/cli/execution_request_command.rb +4 -4
  43. data/lib/morpheus/cli/groups.rb +84 -132
  44. data/lib/morpheus/cli/hosts.rb +6 -16
  45. data/lib/morpheus/cli/instances.rb +100 -183
  46. data/lib/morpheus/cli/invoices_command.rb +505 -71
  47. data/lib/morpheus/cli/library_layouts_command.rb +254 -166
  48. data/lib/morpheus/cli/library_option_lists_command.rb +0 -87
  49. data/lib/morpheus/cli/library_option_types_command.rb +0 -96
  50. data/lib/morpheus/cli/license.rb +3 -0
  51. data/lib/morpheus/cli/login.rb +17 -37
  52. data/lib/morpheus/cli/logout.rb +9 -5
  53. data/lib/morpheus/cli/mixins/accounts_helper.rb +83 -7
  54. data/lib/morpheus/cli/mixins/operations_helper.rb +41 -0
  55. data/lib/morpheus/cli/mixins/option_source_helper.rb +255 -0
  56. data/lib/morpheus/cli/mixins/print_helper.rb +18 -4
  57. data/lib/morpheus/cli/mixins/provisioning_helper.rb +222 -13
  58. data/lib/morpheus/cli/mixins/remote_helper.rb +139 -0
  59. data/lib/morpheus/cli/monitoring_checks_command.rb +11 -3
  60. data/lib/morpheus/cli/network_groups_command.rb +8 -2
  61. data/lib/morpheus/cli/option_types.rb +1 -1
  62. data/lib/morpheus/cli/ping.rb +252 -0
  63. data/lib/morpheus/cli/price_sets_command.rb +16 -27
  64. data/lib/morpheus/cli/prices_command.rb +34 -27
  65. data/lib/morpheus/cli/processes_command.rb +81 -7
  66. data/lib/morpheus/cli/projects_command.rb +607 -0
  67. data/lib/morpheus/cli/recent_activity_command.rb +87 -65
  68. data/lib/morpheus/cli/remote.rb +965 -974
  69. data/lib/morpheus/cli/reports_command.rb +3 -15
  70. data/lib/morpheus/cli/roles.rb +8 -31
  71. data/lib/morpheus/cli/service_plans_command.rb +25 -31
  72. data/lib/morpheus/cli/setup.rb +392 -0
  73. data/lib/morpheus/cli/shell.rb +144 -56
  74. data/lib/morpheus/cli/subnets_command.rb +71 -11
  75. data/lib/morpheus/cli/tasks.rb +3 -3
  76. data/lib/morpheus/cli/user_sources_command.rb +4 -4
  77. data/lib/morpheus/cli/users.rb +135 -109
  78. data/lib/morpheus/cli/version.rb +1 -1
  79. data/lib/morpheus/cli/whitelabel_settings_command.rb +7 -7
  80. data/lib/morpheus/cli/whoami.rb +90 -129
  81. data/lib/morpheus/cli/wiki_command.rb +2 -14
  82. data/lib/morpheus/ext/rest_client.rb +36 -0
  83. data/lib/morpheus/formatters.rb +42 -5
  84. data/lib/morpheus/rest_client.rb +0 -10
  85. data/lib/morpheus/terminal.rb +41 -1
  86. data/lib/morpheus/util.rb +24 -0
  87. metadata +16 -3
  88. data/lib/morpheus/cli/command_error.rb +0 -22
@@ -39,30 +39,20 @@ class Morpheus::Cli::PriceSetsCommand
39
39
  end
40
40
  optparse.parse!(args)
41
41
  connect(options)
42
- if args.count != 0
43
- raise_command_error "wrong number of arguments, expected 0 and got (#{args.count}) #{args}\n#{optparse}"
44
- return 1
42
+ params.merge!(parse_list_options(options))
43
+ params['phrase'] = args.join(' ') if args.count > 0 && params['phrase'].nil? # pass args as phrase, every list command should do this
44
+ @price_sets_interface.setopts(options)
45
+ if options[:dry_run]
46
+ print_dry_run @price_sets_interface.dry.list(params)
47
+ return
45
48
  end
46
-
47
- begin
48
- params.merge!(parse_list_options(options))
49
-
50
- @price_sets_interface.setopts(options)
51
- if options[:dry_run]
52
- print_dry_run @price_sets_interface.dry.list(params)
53
- return
54
- end
55
- json_response = @price_sets_interface.list(params)
56
-
57
- render_result = render_with_format(json_response, options, 'priceSets')
58
- return 0 if render_result
59
-
49
+ json_response = @price_sets_interface.list(params)
50
+ price_sets = json_response['priceSets']
51
+ render_response(json_response, options, 'priceSets') do
60
52
  title = "Morpheus Price Sets"
61
53
  subtitles = []
62
54
  subtitles += parse_list_subtitles(options)
63
55
  print_h1 title, subtitles
64
-
65
- price_sets = json_response['priceSets']
66
56
  if price_sets.empty?
67
57
  print cyan,"No price sets found.",reset,"\n"
68
58
  else
@@ -71,25 +61,24 @@ class Morpheus::Cli::PriceSetsCommand
71
61
  id: (it['active'] ? cyan : yellow) + it['id'].to_s,
72
62
  name: it['name'],
73
63
  active: format_boolean(it['active']),
74
- price_unit: it['priceUnit'],
64
+ priceUnit: it['priceUnit'],
75
65
  type: price_set_type_label(it['type']),
76
66
  price_count: it['prices'].count.to_s + cyan
77
67
  }
78
68
  end
79
69
  columns = [
80
- :id, :name, :active, :price_unit, :type, '# of prices' => :price_count
70
+ :id, :name, :active, :priceUnit, :type, '# OF PRICES' => :price_count
81
71
  ]
82
72
  columns.delete(:active) if !params['includeInactive']
83
-
84
73
  print as_pretty_table(rows, columns, options)
85
74
  print_results_pagination(json_response)
86
- print reset,"\n"
87
75
  end
88
76
  print reset,"\n"
89
- return 0
90
- rescue RestClient::Exception => e
91
- print_rest_exception(e, options)
92
- exit 1
77
+ end
78
+ if price_sets.empty?
79
+ return 1, "0 price sets found"
80
+ else
81
+ return 0, nil
93
82
  end
94
83
  end
95
84
 
@@ -30,35 +30,40 @@ class Morpheus::Cli::PricesCommand
30
30
  opts.on('-i', '--include-inactive [on|off]', String, "Can be used to enable / disable inactive filter. Default is on") do |val|
31
31
  params['includeInactive'] = val.to_s == 'on' || val.to_s == 'true' || val.to_s == '1' || val.to_s == ''
32
32
  end
33
+ opts.on('--platform PLATFORM', Array, "Filter by platform eg. linux, windows") do |val|
34
+ params['platform'] = val.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact
35
+ end
36
+ opts.on('--price-unit UNIT', Array, "Filter by priceUnit eg. hour, month") do |val|
37
+ params['priceUnit'] = val.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact
38
+ end
39
+ opts.on('--currency CURRENCY', Array, "Filter by currency eg. usd") do |val|
40
+ params['currency'] = val.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact
41
+ end
42
+ opts.on('--price-type TYPE', Array, "Filter by priceType eg. fixed,platform,software,compute,storage,datastore,memory,cores,cpu") do |val|
43
+ params['priceType'] = val.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact
44
+ end
33
45
  build_common_options(opts, options, [:list, :query, :json, :yaml, :csv, :fields, :dry_run, :remote])
34
46
  opts.footer = "List prices."
35
47
  end
36
48
  optparse.parse!(args)
49
+ #verify_args!(args:args, optparse:optparse, count:0)
37
50
  connect(options)
38
- if args.count != 0
39
- raise_command_error "wrong number of arguments, expected 0 and got (#{args.count}) #{args}\n#{optparse}"
40
- return 1
51
+
52
+ params.merge!(parse_list_options(options))
53
+ params['phrase'] = args.join(' ') if args.count > 0 && params['phrase'].nil? # pass args as phrase, every list command should do this
54
+ load_whoami()
55
+ @prices_interface.setopts(options)
56
+ if options[:dry_run]
57
+ print_dry_run @prices_interface.dry.list(params)
58
+ return
41
59
  end
42
-
43
- begin
44
- params.merge!(parse_list_options(options))
45
-
46
- @prices_interface.setopts(options)
47
- if options[:dry_run]
48
- print_dry_run @prices_interface.dry.list(params)
49
- return
50
- end
51
- json_response = @prices_interface.list(params)
52
-
53
- render_result = render_with_format(json_response, options, 'prices')
54
- return 0 if render_result
55
-
60
+ json_response = @prices_interface.list(params)
61
+ prices = json_response['prices']
62
+ render_response(json_response, options, 'prices') do
56
63
  title = "Morpheus Prices"
57
64
  subtitles = []
58
65
  subtitles += parse_list_subtitles(options)
59
66
  print_h1 title, subtitles
60
-
61
- prices = json_response['prices']
62
67
  if prices.empty?
63
68
  print yellow,"No prices found.",reset,"\n"
64
69
  else
@@ -77,20 +82,17 @@ class Morpheus::Cli::PricesCommand
77
82
  }
78
83
  end
79
84
  columns = [
80
- :id, :name, :active, :priceType, :tenant, :priceUnit, :priceAdjustment, :cost, :markup, :price
85
+ :id, :name, :active, {'PRICE TYPE' => :priceType}, :tenant, {'PRICE UNIT' => :priceUnit}, {'PRICE ADJUSTMENT' => :priceAdjustment}, :cost, :markup, :price
81
86
  ]
82
87
  columns.delete(:active) if !params['includeInactive']
83
88
 
84
89
  print as_pretty_table(rows, columns, options)
85
90
  print_results_pagination(json_response)
86
- print reset,"\n"
87
91
  end
88
92
  print reset,"\n"
89
- return 0
90
- rescue RestClient::Exception => e
91
- print_rest_exception(e, options)
92
- exit 1
93
93
  end
94
+ return 1, "0 prices found" if prices.empty?
95
+ return 0, nil
94
96
  end
95
97
 
96
98
  def get(args)
@@ -552,7 +554,12 @@ class Morpheus::Cli::PricesCommand
552
554
  end
553
555
 
554
556
  def currency_sym(currency)
555
- Money::Currency.new((currency || 'usd').to_sym).symbol
557
+ begin
558
+ Money::Currency.new((currency.to_s.empty? ? 'usd' : currency).to_sym).symbol
559
+ rescue
560
+ # sometimes '' is getting passed in here, so figure that out...
561
+ Money::Currency.new(:usd).symbol
562
+ end
556
563
  end
557
564
 
558
565
  def price_prefix(price)
@@ -570,7 +577,7 @@ class Morpheus::Cli::PricesCommand
570
577
  end
571
578
 
572
579
  def price_type_label(type)
573
- price_types[type] || type.capitalize
580
+ price_types[type] || type.to_s.capitalize
574
581
  end
575
582
 
576
583
  def price_types
@@ -4,6 +4,9 @@ require 'morpheus/cli/mixins/processes_helper'
4
4
  class Morpheus::Cli::Processes
5
5
  include Morpheus::Cli::CliCommand
6
6
  include Morpheus::Cli::ProcessesHelper
7
+ include Morpheus::Cli::ProvisioningHelper
8
+ include Morpheus::Cli::InfrastructureHelper
9
+ include Morpheus::Cli::OptionSourceHelper
7
10
 
8
11
  set_command_name :'process'
9
12
 
@@ -16,9 +19,11 @@ class Morpheus::Cli::Processes
16
19
  #@appliance_name, @appliance_url = Morpheus::Cli::Remote.active_appliance
17
20
  end
18
21
 
19
- def connect(opts)
20
- @api_client = establish_remote_appliance_connection(opts)
22
+ def connect(options)
23
+ @api_client = establish_remote_appliance_connection(options)
21
24
  @processes_interface = @api_client.processes
25
+ #@instances_interface = @api_client.instances
26
+ @clouds_interface = @api_client.clouds
22
27
  end
23
28
 
24
29
  def handle(args)
@@ -42,21 +47,29 @@ class Morpheus::Cli::Processes
42
47
  options[:show_output] = true
43
48
  options[:details] = true
44
49
  end
45
- opts.on('--app ID', String, "Limit results to specific app(s).") do |val|
50
+ opts.on('--app APP', String, "Limit results to specific app(s).") do |val|
46
51
  params['appIds'] = val.split(',').collect {|it| it.to_s.strip }.reject { |it| it.empty? }
47
52
  end
48
- opts.on('--instance ID', String, "Limit results to specific instance(s).") do |val|
53
+ opts.on('--instance INSTANCE', String, "Limit results to specific instance(s).") do |val|
49
54
  params['instanceIds'] = val.split(',').collect {|it| it.to_s.strip }.reject { |it| it.empty? }
50
55
  end
51
- opts.on('--container ID', String, "Limit results to specific container(s).") do |val|
56
+ opts.on('--container CONTAINER', String, "Limit results to specific container(s).") do |val|
52
57
  params['containerIds'] = val.split(',').collect {|it| it.to_s.strip }.reject { |it| it.empty? }
53
58
  end
54
- opts.on('--host ID', String, "Limit results to specific host(s).") do |val|
59
+ opts.on('--host HOST', String, "Limit results to specific host(s).") do |val|
55
60
  params['serverIds'] = val.split(',').collect {|it| it.to_s.strip }.reject { |it| it.empty? }
56
61
  end
57
- opts.on('--cloud ID', String, "Limit results to specific cloud(s).") do |val|
62
+ opts.on('--server HOST', String, "Limit results to specific servers(s).") do |val|
63
+ params['serverIds'] = val.split(',').collect {|it| it.to_s.strip }.reject { |it| it.empty? }
64
+ end
65
+ opts.add_hidden_option('--server')
66
+ opts.on('--cloud CLOUD', String, "Limit results to specific cloud(s).") do |val|
58
67
  params['zoneIds'] = val.split(',').collect {|it| it.to_s.strip }.reject { |it| it.empty? }
59
68
  end
69
+ opts.on('--user USER', String, "Limit results to user(s).") do |val|
70
+ #params['userId'] = val.split(',').collect {|it| it.to_s.strip }.reject { |it| it.empty? }
71
+ options[:user] = val
72
+ end
60
73
  build_common_options(opts, options, [:list, :query, :json, :yaml, :csv, :fields, :dry_run, :remote])
61
74
  opts.footer = "List historical processes."
62
75
  end
@@ -69,6 +82,67 @@ class Morpheus::Cli::Processes
69
82
  connect(options)
70
83
  begin
71
84
  params.merge!(parse_list_options(options))
85
+ if params['instanceIds']
86
+ params['instanceIds'] = params['instanceIds'].collect do |instance_id|
87
+ if instance_id.to_s =~ /\A\d{1,}\Z/
88
+ # just allow instance IDs
89
+ instance_id.to_i
90
+ else
91
+ instance = find_instance_by_name_or_id(instance_id)
92
+ if instance.nil?
93
+ return 1, "instance not found for '#{instance_id}'" # never happens because find exits
94
+ end
95
+ instance['id']
96
+ end
97
+ end
98
+ end
99
+ if params['serverIds']
100
+ params['serverIds'] = params['serverIds'].collect do |server_id|
101
+ if server_id.to_s =~ /\A\d{1,}\Z/
102
+ # just allow server IDs
103
+ server_id.to_i
104
+ else
105
+ server = find_server_by_name_or_id(server_id)
106
+ if server.nil?
107
+ return 1, "server not found for '#{server_id}'" # never happens because find exits
108
+ end
109
+ server['id']
110
+ end
111
+ end
112
+ end
113
+ if params['appIds']
114
+ params['appIds'] = params['appIds'].collect do |app_id|
115
+ if app_id.to_s =~ /\A\d{1,}\Z/
116
+ # just allow app IDs
117
+ app_id.to_i
118
+ else
119
+ app = find_app_by_name_or_id(app_id)
120
+ if app.nil?
121
+ return 1, "app not found for '#{app_id}'" # never happens because find exits
122
+ end
123
+ app['id']
124
+ end
125
+ end
126
+ end
127
+ if params['zoneIds']
128
+ params['zoneIds'] = params['zoneIds'].collect do |zone_id|
129
+ if zone_id.to_s =~ /\A\d{1,}\Z/
130
+ # just allow zone IDs
131
+ zone_id.to_i
132
+ else
133
+ zone = find_cloud_by_name_or_id(zone_id)
134
+ if zone.nil?
135
+ return 1, "cloud not found for '#{zone_id}'" # never happens because find exits
136
+ end
137
+ zone['id']
138
+ end
139
+ end
140
+ end
141
+ if options[:user]
142
+ user = find_available_user_option(options[:user])
143
+ return 1, "user not found by '#{options[:user]}'" if user.nil?
144
+ params['userId'] = user['id']
145
+ end
72
146
  @processes_interface.setopts(options)
73
147
  if options[:dry_run]
74
148
  print_dry_run @processes_interface.dry.list(params)
@@ -0,0 +1,607 @@
1
+ require 'morpheus/cli/cli_command'
2
+
3
+ class Morpheus::Cli::Projects
4
+ include Morpheus::Cli::CliCommand
5
+ include Morpheus::Cli::ProvisioningHelper
6
+ include Morpheus::Cli::OptionSourceHelper
7
+ # include Morpheus::Cli::InfrastructureHelper
8
+ # include Morpheus::Cli::AccountsHelper
9
+
10
+ register_subcommands :list, :get, :add, :update, :remove,
11
+ # :add_tags, :remove_tags,
12
+ #:add_instance, :remove_instance
13
+ #:add_host, :remove_host
14
+
15
+ def connect(opts)
16
+ @api_client = establish_remote_appliance_connection(opts)
17
+ @projects_interface = @api_client.projects
18
+ @instances_interface = @api_client.instances
19
+ @servers_interface = @api_client.servers
20
+ @clouds_interface = @api_client.clouds
21
+ end
22
+
23
+ def handle(args)
24
+ handle_subcommand(args)
25
+ end
26
+
27
+ def list(args)
28
+ params = {}
29
+ options = {}
30
+ instance_ids, server_ids, cloud_ids = nil, nil, nil
31
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
32
+ opts.banner = subcommand_usage()
33
+ opts.on('--instances [LIST]', Array, "Filter by Instance, comma separated list of instance names or IDs.") do |list|
34
+ instance_ids = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
35
+ end
36
+ opts.on('--servers [LIST]', Array, "Filter by Server, comma separated list of server (host) names or IDs.") do |list|
37
+ server_ids = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
38
+ end
39
+ opts.on('--clouds [LIST]', Array, "Filter by Cloud, comma separated list of cloud (zone) names or IDs.") do |list|
40
+ cloud_ids = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
41
+ end
42
+ # opts.on('--owner [LIST]', Array, "Owner, comma separated list of usernames or IDs.") do |list|
43
+ # params['ownerId'] = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
44
+ # end
45
+ build_standard_get_options(opts, options)
46
+ opts.footer = <<-EOT
47
+ List projects.
48
+ EOT
49
+ end
50
+ optparse.parse!(args)
51
+ #verify_args!(args:args, optparse:optparse, count:0)
52
+ connect(options)
53
+ options[:phrase] = args.join(' ') if args.count > 0 && params[:phrase].nil? # pass args as phrase, every list command should do this
54
+ params.merge!(parse_list_options(options))
55
+ params['instanceId'] = parse_instance_id_list(instance_ids) if instance_ids
56
+ # todo server and cloud, missing parse_server_id_list() too
57
+ params['serverId'] = parse_server_id_list(server_ids) if server_ids
58
+ params['cloudId'] = parse_cloud_id_list(cloud_ids) if cloud_ids
59
+ @projects_interface.setopts(options)
60
+ if options[:dry_run]
61
+ print_dry_run @projects_interface.dry.list(params)
62
+ return
63
+ end
64
+ json_response = @projects_interface.list(params)
65
+ projects = json_response['projects']
66
+ render_response(json_response, options, 'projects') do
67
+ title = "Morpheus Projects"
68
+ subtitles = []
69
+ subtitles += parse_list_subtitles(options)
70
+ print_h1 title, subtitles
71
+ if projects.empty?
72
+ print yellow,"No projects found.",reset,"\n"
73
+ else
74
+ print cyan
75
+ columns = [
76
+ {"ID" => lambda {|it| it['id'] } },
77
+ {"NAME" => lambda {|it| it['name'] } },
78
+ {"DESCRIPTION" => lambda {|it| it['description'] } },
79
+ {"INSTANCES" => lambda {|it| it['instances'].size rescue '' } },
80
+ {"SERVERS" => lambda {|it| it['servers'].size rescue '' } },
81
+ {"CLOUDS" => lambda {|it| it['clouds'].size rescue '' } },
82
+ {"OWNER" => lambda {|it| it['owner'] ? it['owner']['username'] : '' } },
83
+ {"DATE CREATED" => lambda {|it| format_local_dt(it['dateCreated']) } },
84
+ {"LAST UPDATED" => lambda {|it| format_local_dt(it['lastUpdated']) } },
85
+ ]
86
+ print as_pretty_table(projects, columns, options)
87
+ print_results_pagination(json_response)
88
+ end
89
+ print reset,"\n"
90
+ end
91
+ if projects.empty?
92
+ return 1, "No projects found"
93
+ else
94
+ return 0, nil
95
+ end
96
+ end
97
+
98
+ def get(args)
99
+ options = {}
100
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
101
+ opts.banner = subcommand_usage("[workflow]")
102
+ opts.on('--no-content', "Do not display script content." ) do
103
+ options[:no_content] = true
104
+ end
105
+ build_common_options(opts, options, [:json, :yaml, :csv, :fields, :dry_run, :remote])
106
+ opts.footer = <<-EOT
107
+ Get details about a project.
108
+ [project] is required. This is the name or id of a project.
109
+ EOT
110
+ end
111
+ optparse.parse!(args)
112
+ verify_args!(args:args, optparse:optparse, min:1)
113
+ connect(options)
114
+ id_list = parse_id_list(args)
115
+ return run_command_for_each_arg(id_list) do |arg|
116
+ _get(arg, options)
117
+ end
118
+ end
119
+
120
+ def _get(id, options)
121
+ @projects_interface.setopts(options)
122
+ if options[:dry_run]
123
+ if id.to_s =~ /\A\d{1,}\Z/
124
+ print_dry_run @projects_interface.dry.get(id.to_i)
125
+ else
126
+ print_dry_run @projects_interface.dry.get({name: id})
127
+ end
128
+ return
129
+ end
130
+ project = find_project_by_name_or_id(id)
131
+ exit 1 if project.nil?
132
+ # refetch it by id
133
+ json_response = {'project' => project}
134
+ unless id.to_s =~ /\A\d{1,}\Z/
135
+ json_response = @projects_interface.get(project['id'])
136
+ end
137
+ project = json_response['project']
138
+ render_response(json_response, options, 'project') do
139
+ print_h1 "Project Details"
140
+ print cyan
141
+ description_cols = {
142
+ "ID" => 'id',
143
+ "Name" => 'name',
144
+ "Description" => 'description',
145
+ "Tags" => lambda {|it| it['tags'] ? it['tags'].collect {|m| "#{m['name']}: #{m['value']}" }.join(', ') : '' },
146
+ "Owner" => lambda {|it| it['owner'] ? it['owner']['username'] : '' },
147
+ "Date Created" => lambda {|it| format_local_dt(it['dateCreated']) },
148
+ "Last Updated" => lambda {|it| format_local_dt(it['lastUpdated']) }
149
+ }
150
+ print_description_list(description_cols, project)
151
+
152
+ project_instances = project["instances"]
153
+ if project_instances && project_instances.size > 0
154
+ print_h2 "Instances"
155
+ print cyan
156
+ print as_pretty_table(project_instances, [:id, :name], options)
157
+ end
158
+
159
+ project_hosts = project["servers"] || project["hosts"]
160
+ if project_hosts && project_hosts.size > 0
161
+ print_h2 "Hosts"
162
+ print cyan
163
+ print as_pretty_table(project_hosts, [:id, :name], options)
164
+ end
165
+
166
+ project_clouds = project["clouds"] || project["zones"]
167
+ if project_clouds && project_clouds.size > 0
168
+ print_h2 "Clouds"
169
+ print cyan
170
+ print as_pretty_table(project_clouds, [:id, :name], options)
171
+ end
172
+ print reset,"\n"
173
+ end
174
+ return 0, nil
175
+ end
176
+
177
+ def add(args)
178
+ exit_code, err = 0, nil
179
+ params, options, payload = {}, {}, {}
180
+ project_name, description, metadata, instance_ids, server_ids, cloud_ids = nil, nil, nil, nil, nil, nil
181
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
182
+ opts.banner = subcommand_usage("[name]")
183
+ opts.on('--name NAME', String, "Project Name" ) do |val|
184
+ project_name = val
185
+ end
186
+ opts.on('--description NAME', String, "Project Description" ) do |val|
187
+ description = val
188
+ end
189
+ opts.on('--tags TAGS', String, "Tags in the format 'name:value, name:value'") do |val|
190
+ metadata = val
191
+ end
192
+ opts.on('--instances [LIST]', Array, "Instances, comma separated list of instance names or IDs.") do |list|
193
+ instance_ids = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
194
+ end
195
+ opts.on('--servers [LIST]', Array, "Servers, comma separated list of server (host) names or IDs.") do |list|
196
+ server_ids = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
197
+ end
198
+ opts.on('--clouds [LIST]', Array, "Clouds, comma separated list of cloud names or IDs.") do |list|
199
+ cloud_ids = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
200
+ end
201
+ build_standard_add_options(opts, options)
202
+ opts.footer = <<-EOT
203
+ Create a project.
204
+ [name] is required. This is the name of the new project.
205
+ EOT
206
+ end
207
+ optparse.parse!(args)
208
+ verify_args!(args:args, optparse:optparse, max:1)
209
+ if args[0]
210
+ project_name = args[0]
211
+ end
212
+ connect(options)
213
+ exit_code, err = 0, nil
214
+ # construct payload
215
+ if options[:payload]
216
+ payload = options[:payload]
217
+ payload.deep_merge!({'project' => parse_passed_options(options)})
218
+ else
219
+ payload['project'] = {}
220
+ payload.deep_merge!({'project' => parse_passed_options(options)})
221
+ payload['project']['name'] = project_name ? project_name : Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true, 'description' => 'Project Name'}], options[:options], @api_client)['name']
222
+ payload['project']['description'] = description ? description : Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'description', 'fieldLabel' => 'Description', 'type' => 'text', 'required' => false, 'description' => 'Project Description'}], options[:options], @api_client)['description']
223
+ # metadata tags
224
+ if metadata
225
+ metadata = parse_metadata(metadata)
226
+ payload['project']['tags'] = metadata if metadata
227
+ else
228
+ metadata = prompt_metadata(options)
229
+ payload['project']['tags'] = metadata if metadata
230
+ end
231
+ if instance_ids != nil
232
+ payload['project']['instances'] = parse_instance_id_list(instance_ids).collect { |it| {'id': it.to_i} }
233
+ else
234
+ # could prompt
235
+ end
236
+ # --instances
237
+ if instance_ids
238
+ payload['project']['instances'] = parse_instance_id_list(instance_ids).collect { |it| {'id': it.to_i} }
239
+ end
240
+ # --servers
241
+ if server_ids
242
+ payload['project']['servers'] = parse_server_id_list(server_ids).collect { |it| {'id': it.to_i} }
243
+ end
244
+ # --clouds
245
+ if cloud_ids
246
+ payload['project']['clouds'] = parse_cloud_id_list(cloud_ids).collect { |it| {'id': it.to_i} }
247
+ end
248
+ end
249
+ @projects_interface.setopts(options)
250
+ if options[:dry_run]
251
+ print_dry_run @projects_interface.dry.create(payload)
252
+ return
253
+ end
254
+ json_response = @projects_interface.create(payload)
255
+ project = json_response['project']
256
+ render_response(json_response, options, 'project') do
257
+ print_green_success "Project #{project['name']} created"
258
+ exit_code, err = get([project['id']] + (options[:remote] ? ["-r",options[:remote]] : []))
259
+ end
260
+ return exit_code, err
261
+ end
262
+
263
+ def update(args)
264
+ exit_code, err = 0, nil
265
+ params, options, payload = {}, {}, {}
266
+ project_name, description, metadata, add_metadata, remove_metadata = nil, nil, nil, nil, nil
267
+ instance_ids, server_ids, cloud_ids = nil, nil, nil
268
+ add_instance_ids, remove_instance_ids = nil, nil
269
+ add_server_ids, remove_server_ids = nil, nil
270
+ add_cloud_ids, remove_cloud_ids = nil, nil
271
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
272
+ opts.banner = subcommand_usage("[project] [options]")
273
+ opts.on('--name NAME', String, "Project Name" ) do |val|
274
+ project_name = val
275
+ end
276
+ opts.on('--description NAME', String, "Project Description" ) do |val|
277
+ description = val
278
+ end
279
+ opts.on('--tags [TAGS]', String, "Project Tags in the format 'name:value, name:value'. This replaces all project tags.") do |val|
280
+ metadata = val ? val : []
281
+ end
282
+ opts.on('--add-tags TAGS', String, "Add Project Tags in the format 'name:value, name:value'. This will add/update project tags.") do |val|
283
+ add_metadata = val
284
+ end
285
+ opts.on('--remove-tags TAGS', String, "Remove Project Tags in the format 'name, name:value'. This removes project tags, the :value component is optional and must match if passed.") do |val|
286
+ remove_metadata = val
287
+ end
288
+ opts.on('--instances [LIST]', Array, "Instances, comma separated list of instance names or IDs.") do |list|
289
+ instance_ids = list ? list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq : []
290
+ end
291
+ opts.on('--add-instances LIST', Array, "Add Instances, comma separated list of instance names or IDs to add.") do |list|
292
+ add_instance_ids = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
293
+ end
294
+ opts.on('--remove-instances LIST', Array, "Remove Instances, comma separated list of instance names or IDs to remove.") do |list|
295
+ remove_instance_ids = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
296
+ end
297
+ opts.on('--servers [LIST]', Array, "Servers, comma separated list of server (host) names or IDs.") do |list|
298
+ server_ids = list ? list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq : []
299
+ end
300
+ opts.on('--add-servers LIST', Array, "Add Servers, comma separated list of server names or IDs to add.") do |list|
301
+ add_server_ids = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
302
+ end
303
+ opts.on('--remove-servers LIST', Array, "Remove Servers, comma separated list of server names or IDs to remove.") do |list|
304
+ remove_server_ids = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
305
+ end
306
+ opts.on('--clouds [LIST]', Array, "Clouds, comma separated list of cloud names or IDs.") do |list|
307
+ cloud_ids = list ? list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq : []
308
+ end
309
+ opts.on('--add-clouds LIST', Array, "Add Clouds, comma separated list of cloud names or IDs to add.") do |list|
310
+ add_cloud_ids = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
311
+ end
312
+ opts.on('--remove-clouds LIST', Array, "Remove Clouds, comma separated list of cloud names or IDs to remove.") do |list|
313
+ remove_cloud_ids = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
314
+ end
315
+ build_standard_update_options(opts, options)
316
+ opts.footer = <<-EOT
317
+ Update a project.
318
+ [project] is required. This is the name or id of a project.
319
+ EOT
320
+ end
321
+ optparse.parse!(args)
322
+ verify_args!(args:args, optparse:optparse, count:1)
323
+ connect(options)
324
+ exit_code, err = 0, nil
325
+ project = find_project_by_name_or_id(args[0])
326
+ return 1, "project not found by '#{args[0]}'" if project.nil?
327
+ # construct payload
328
+ if options[:payload]
329
+ payload = options[:payload]
330
+ payload.deep_merge!({'project' => parse_passed_options(options)})
331
+ else
332
+ payload['project'] = {}
333
+ payload.deep_merge!({'project' => parse_passed_options(options)})
334
+ payload['project']['name'] = project_name if project_name
335
+ payload['project']['description'] = description if description
336
+ # metadata tags
337
+ if metadata
338
+ payload['project']['tags'] = parse_metadata(metadata)
339
+ end
340
+ if add_metadata
341
+ payload['project']['addTags'] = parse_metadata(add_metadata)
342
+ end
343
+ if remove_metadata
344
+ payload['project']['removeTags'] = parse_metadata(remove_metadata)
345
+ end
346
+ # --instances
347
+ if instance_ids
348
+ payload['project']['instances'] = parse_instance_id_list(instance_ids).collect { |it| {'id': it.to_i} }
349
+ end
350
+ if add_instance_ids
351
+ payload['project']['addInstances'] = parse_instance_id_list(add_instance_ids).collect { |it| {'id': it.to_i} }
352
+ end
353
+ if remove_instance_ids
354
+ payload['project']['removeInstances'] = parse_instance_id_list(remove_instance_ids).collect { |it| {'id': it.to_i} }
355
+ end
356
+ # --servers
357
+ if server_ids
358
+ payload['project']['servers'] = parse_server_id_list(server_ids).collect { |it| {'id': it.to_i} }
359
+ end
360
+ if add_server_ids
361
+ payload['project']['addServers'] = parse_server_id_list(add_server_ids).collect { |it| {'id': it.to_i} }
362
+ end
363
+ if remove_server_ids
364
+ payload['project']['removeServers'] = parse_server_id_list(remove_server_ids).collect { |it| {'id': it.to_i} }
365
+ end
366
+ # --clouds
367
+ if cloud_ids
368
+ cloud_ids = parse_cloud_id_list(cloud_ids)
369
+ return 1, "clouds not found" if cloud_ids.nil?
370
+ payload['project']['clouds'] = cloud_ids.collect { |it| {'id': it.to_i} }
371
+ end
372
+ if add_cloud_ids
373
+ add_cloud_ids = parse_cloud_id_list(add_cloud_ids)
374
+ return 1, "clouds not found" if add_cloud_ids.nil?
375
+ payload['project']['addClouds'] = add_cloud_ids.collect { |it| {'id': it.to_i} }
376
+ end
377
+ if remove_cloud_ids
378
+ remove_cloud_ids = parse_cloud_id_list(remove_cloud_ids)
379
+ return 1, "clouds not found" if remove_cloud_ids.nil?
380
+ payload['project']['removeClouds'] = remove_cloud_ids.collect { |it| {'id': it.to_i} }
381
+ end
382
+ end
383
+ @projects_interface.setopts(options)
384
+ if options[:dry_run]
385
+ print_dry_run @projects_interface.dry.update(project['id'], payload)
386
+ return
387
+ end
388
+ json_response = @projects_interface.update(project['id'], payload)
389
+ project = json_response['project']
390
+ render_response(json_response, options, 'project') do
391
+ print_green_success "Project #{project['name']} updated"
392
+ exit_code, err = get([project['id']] + (options[:remote] ? ["-r",options[:remote]] : []))
393
+ end
394
+ return exit_code, err
395
+ end
396
+
397
+ def add_tags(args)
398
+ params = {}
399
+ options = {}
400
+ payload = {}
401
+ metadata = nil
402
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
403
+ opts.banner = subcommand_usage("[project] [tag=value] [tag=value]")
404
+ opts.on('--tags TAGS', String, "Project Tags in the format 'name:value, name:value'. This will add/update project tags.") do |val|
405
+ metadata = val
406
+ end
407
+ build_standard_update_options(opts, options)
408
+ opts.footer = <<-EOT
409
+ Add metadata tags to a project.
410
+ [project] is required. This is the name or id of a project.
411
+ [tag=value] is required. This is a metadata tag in the format name=value, 1-N may be passed.
412
+ EOT
413
+ end
414
+ optparse.parse!(args)
415
+ verify_args!(args:args, optparse:optparse, min:1) # maybe min:2 instead ?
416
+ connect(options)
417
+ exit_code, err = 0, nil
418
+ if args.count > 1
419
+ metadata = args[1..-1].join(" ")
420
+ end
421
+ project = find_project_by_name_or_id(args[0])
422
+ return 1, "project not found by '#{args[0]}'" if project.nil?
423
+ # construct payload
424
+
425
+ if options[:payload]
426
+ payload = options[:payload]
427
+ payload.deep_merge!({'project' => parse_passed_options(options)})
428
+ else
429
+ payload['project'] = {}
430
+ payload.deep_merge!({'project' => parse_passed_options(options)})
431
+ if options[:metadata]
432
+ payload['project']['addTags'] = parse_metadata(metadata)
433
+ end
434
+ end
435
+ @projects_interface.setopts(options)
436
+ if options[:dry_run]
437
+ print_dry_run @projects_interface.dry.update(project['id'], payload)
438
+ return
439
+ end
440
+ json_response = @projects_interface.update(project['id'], payload)
441
+ project = json_response['project']
442
+ render_response(json_response, options, 'project') do
443
+ print_green_success "Project #{project['name']} updated"
444
+ exit_code, err = get([project['id']] + (options[:remote] ? ["-r",options[:remote]] : []))
445
+ end
446
+ return exit_code, err
447
+ end
448
+
449
+ def remove_tags(args)
450
+ params = {}
451
+ options = {}
452
+ payload = {}
453
+ metadata = nil
454
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
455
+ opts.banner = subcommand_usage("[project] [tags]")
456
+ opts.on('--tags TAGS', String, "Project Tags in the format 'name=value, name=value'. This will add/update project tags.") do |val|
457
+ metadata = val
458
+ end
459
+ build_standard_update_options(opts, options)
460
+ opts.footer = <<-EOT
461
+ Remove metadata tags from a project.
462
+ [project] is required. This is the name or id of a project.
463
+ [tags] is required. This is a metadata tag in the format tag=value, tag2, tag3, 1-N may be passed, value is optional and must match if passed.
464
+ EOT
465
+ end
466
+ optparse.parse!(args)
467
+ verify_args!(args:args, optparse:optparse, min:1) # maybe min:2 instead ?
468
+ connect(options)
469
+ exit_code, err = 0, nil
470
+ if args.count > 1
471
+ metadata = args[1..-1].join(" ")
472
+ end
473
+ project = find_project_by_name_or_id(args[0])
474
+ return 1, "project not found by '#{args[0]}'" if project.nil?
475
+ # construct payload
476
+
477
+ if options[:payload]
478
+ payload = options[:payload]
479
+ payload.deep_merge!({'project' => parse_passed_options(options)})
480
+ else
481
+ payload['project'] = {}
482
+ payload.deep_merge!({'project' => parse_passed_options(options)})
483
+ if metadata
484
+ payload['project']['removeTags'] = parse_metadata(metadata)
485
+ end
486
+ end
487
+ @projects_interface.setopts(options)
488
+ if options[:dry_run]
489
+ print_dry_run @projects_interface.dry.update(project['id'], payload)
490
+ return
491
+ end
492
+ json_response = @projects_interface.update(project['id'], payload)
493
+ project = json_response['project']
494
+ render_response(json_response, options, 'project') do
495
+ print_green_success "Project #{project['name']} updated"
496
+ exit_code, err = get([project['id']] + (options[:remote] ? ["-r",options[:remote]] : []))
497
+ end
498
+ return exit_code, err
499
+ end
500
+
501
+ def remove(args)
502
+ params = {}
503
+ options = {}
504
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
505
+ opts.banner = subcommand_usage("[project]")
506
+ build_common_options(opts, options, [:auto_confirm, :json, :dry_run, :quiet, :remote])
507
+ # opts.on( '-f', '--force', "Force Delete" ) do
508
+ # params[:force] = true
509
+ # end
510
+ opts.footer = <<-EOT
511
+ Delete a project.
512
+ [project] is required. This is the name or id of a project.
513
+ EOT
514
+ end
515
+ optparse.parse!(args)
516
+ verify_args!(args:args, optparse:optparse, count:1)
517
+ connect(options)
518
+ exit_code, err = 0, nil
519
+ project = find_project_by_name_or_id(args[0])
520
+ return 1, "project not found by '#{args[0]}'" if project.nil?
521
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the project #{project['name']}?")
522
+ return 9, "aborted command"
523
+ end
524
+ @projects_interface.setopts(options)
525
+ if options[:dry_run]
526
+ print_dry_run @projects_interface.dry.destroy(project['id'], params)
527
+ return
528
+ end
529
+ json_response = @projects_interface.destroy(project['id'], params)
530
+ render_response(json_response, options) do
531
+ print_green_success "Project #{project['name']} removed"
532
+ end
533
+ return exit_code, err
534
+ end
535
+
536
+ private
537
+
538
+ def find_project_by_name_or_id(val)
539
+ if val.to_s =~ /\A\d{1,}\Z/
540
+ return find_project_by_id(val)
541
+ else
542
+ return find_project_by_name(val)
543
+ end
544
+ end
545
+
546
+ def find_project_by_id(id)
547
+ begin
548
+ json_response = @projects_interface.get(id.to_i)
549
+ return json_response['project']
550
+ rescue RestClient::Exception => e
551
+ if e.response && e.response.code == 404
552
+ print_red_alert "Project not found by id #{id}"
553
+ return nil
554
+ else
555
+ raise e
556
+ end
557
+ end
558
+ end
559
+
560
+ def find_project_by_name(name)
561
+ projects = @projects_interface.list({name: name.to_s})['projects']
562
+ if projects.empty?
563
+ print_red_alert "Project not found by name #{name}"
564
+ return nil
565
+ elsif projects.size > 1
566
+ print_red_alert "#{projects.size} projects by name #{name}"
567
+ print_error "\n"
568
+ puts_error as_pretty_table(projects, [:id, :name], {color:red})
569
+ print_red_alert "Try passing ID instead"
570
+ return nil
571
+ else
572
+ return projects[0]
573
+ end
574
+ end
575
+
576
+ def find_project_type_by_name(val)
577
+ raise "find_project_type_by_name passed a bad name: #{val.inspect}" if val.to_s == ''
578
+ @all_project_types ||= @projects_interface.list_types({max:1000})['projectTypes']
579
+
580
+ if @all_project_types.nil? && !@all_project_types.empty?
581
+ print_red_alert "No project types found"
582
+ return nil
583
+ end
584
+ matching_project_types = @all_project_types.select { |it| val && (it['name'] == val || it['code'] == val || it['id'].to_s == val.to_s) }
585
+ if matching_project_types.size == 1
586
+ return matching_project_types[0]
587
+ elsif matching_project_types.size == 0
588
+ print_red_alert "Project Type not found by '#{val}'"
589
+ else
590
+ print_red_alert "#{matching_project_types.size} project types found by name #{name}"
591
+ rows = matching_project_types.collect do |it|
592
+ {id: it['id'], name: it['name'], code: it['code']}
593
+ end
594
+ print "\n"
595
+ puts as_pretty_table(rows, [:name, :code], {color:red})
596
+ return nil
597
+ end
598
+ end
599
+
600
+ def update_project_option_types(project_type)
601
+ [
602
+ {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true, 'displayOrder' => 0}
603
+ ] + project_type['optionTypes']
604
+ end
605
+
606
+
607
+ end