morpheus-cli 8.0.7 → 8.0.8

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 28b8927c521af95d6c463be3cf8de08ad7f84017e18ab89a8143759d6e11e722
4
- data.tar.gz: 0a1c6677040bcb8e50a5bde9603b5cda373d8ee16c41cf3a74ed96f1ff23a1be
3
+ metadata.gz: df49518b39388f8a75824156a31bff7fb8abcf4545a9f5af29bf6eab4eab6a79
4
+ data.tar.gz: c82269904d1de5f01a487ebad5bd6ac2dd110a0c2e2b183142dfb42828c5edec
5
5
  SHA512:
6
- metadata.gz: 0dca9e1aedcf8daedd76aba8e43b93d56733c05d247aa8fc31b0324d2b22c122e6164519e870a590ff703ad5413b8b96cf822dd537533acd2f2dce53a6fc6697
7
- data.tar.gz: a8cdd96b0895216198267f1b639b97654fa0e0c2c38db49f55eb6776153305d59148febbca8bac8fe81bf193d3b465a90fbcd38bf8b52d508e01bdc3dfc44670
6
+ metadata.gz: b64913eef407dd0180526e67095182b89517fa2863ee8bce319ba77fb1c1ca25d0898299ebebd6072dc9fcbd19a77f8d3f4fef3298563e24bd37121fb8ae6c75
7
+ data.tar.gz: 6569d731faabe620597d94d20f78ffde2b5599934cd2b5bdb7153e86ce6cbd8359b83e0ff97a61d1d8ff6b8c4c8c6fc809a82e72ef39da5514859800e8388e2a
data/Dockerfile CHANGED
@@ -1,5 +1,5 @@
1
1
  FROM ruby:2.7.5
2
2
 
3
- RUN gem install morpheus-cli -v 8.0.7
3
+ RUN gem install morpheus-cli -v 8.0.8
4
4
 
5
5
  ENTRYPOINT ["morpheus"]
@@ -2,6 +2,10 @@ require 'morpheus/api/api_client'
2
2
 
3
3
  class Morpheus::CloudsInterface < Morpheus::APIClient
4
4
 
5
+ def base_path
6
+ "/api/zones"
7
+ end
8
+
5
9
  def cloud_types(params={})
6
10
  url = "#{@base_url}/api/zone-types"
7
11
  headers = { params: params, authorization: "Bearer #{@access_token}" }
@@ -137,4 +141,35 @@ class Morpheus::CloudsInterface < Morpheus::APIClient
137
141
  end
138
142
  execute(method: :post, url: url, headers: headers, payload: payload)
139
143
  end
144
+
145
+ def list_affinity_groups(id, params={})
146
+ url = "#{base_path}/#{id}/affinity-groups"
147
+ headers = { params: params, authorization: "Bearer #{@access_token}" }
148
+ execute(method: :get, url: url, headers: headers)
149
+ end
150
+
151
+ def get_affinity_group(id, affinity_group_id, params={})
152
+ url = "#{base_path}/#{id}/affinity-groups/#{affinity_group_id}"
153
+ headers = { params: params, authorization: "Bearer #{@access_token}" }
154
+ execute(method: :get, url: url, headers: headers)
155
+ end
156
+
157
+ def create_affinity_group(id, payload)
158
+ url = "#{base_path}/#{id}/affinity-groups"
159
+ headers = { :authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
160
+ execute(method: :post, url: url, headers: headers, payload: payload.to_json)
161
+ end
162
+
163
+ def update_affinity_group(id, affinity_group_id, payload)
164
+ url = "#{base_path}/#{id}/affinity-groups/#{affinity_group_id}"
165
+ headers = { :authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
166
+ execute(method: :put, url: url, headers: headers, payload: payload.to_json)
167
+ end
168
+
169
+ def destroy_affinity_group(id, affinity_group_id, params={})
170
+ url = "#{base_path}/#{id}/affinity-groups/#{affinity_group_id}"
171
+ headers = { :params => params, :authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
172
+ execute(method: :delete, url: url, headers: headers)
173
+ end
174
+
140
175
  end
@@ -353,4 +353,34 @@ class Morpheus::ClustersInterface < Morpheus::APIClient
353
353
  execute(method: :get, url: url, headers: headers)
354
354
  end
355
355
 
356
+ def list_affinity_groups(id, params={})
357
+ url = "#{base_path}/#{id}/affinity-groups"
358
+ headers = { params: params, authorization: "Bearer #{@access_token}" }
359
+ execute(method: :get, url: url, headers: headers)
360
+ end
361
+
362
+ def get_affinity_group(id, affinity_group_id, params={})
363
+ url = "#{base_path}/#{id}/affinity-groups/#{affinity_group_id}"
364
+ headers = { params: params, authorization: "Bearer #{@access_token}" }
365
+ execute(method: :get, url: url, headers: headers)
366
+ end
367
+
368
+ def create_affinity_group(id, payload)
369
+ url = "#{base_path}/#{id}/affinity-groups"
370
+ headers = { :authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
371
+ execute(method: :post, url: url, headers: headers, payload: payload.to_json)
372
+ end
373
+
374
+ def update_affinity_group(id, affinity_group_id, payload)
375
+ url = "#{base_path}/#{id}/affinity-groups/#{affinity_group_id}"
376
+ headers = { :authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
377
+ execute(method: :put, url: url, headers: headers, payload: payload.to_json)
378
+ end
379
+
380
+ def destroy_affinity_group(id, affinity_group_id, params={})
381
+ url = "#{base_path}/#{id}/affinity-groups/#{affinity_group_id}"
382
+ headers = { :params => params, :authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
383
+ execute(method: :delete, url: url, headers: headers)
384
+ end
385
+
356
386
  end
@@ -212,5 +212,12 @@ class Morpheus::ServersInterface < Morpheus::APIClient
212
212
  execute(opts)
213
213
  end
214
214
 
215
+ def snapshot(serverId, payload = {}, params = {})
216
+ url = "#{@base_url}/api/servers/#{serverId}/snapshot"
217
+ headers = { :params => params, :authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
218
+ opts = {method: :put, url: url, headers: headers, payload: payload.to_json}
219
+ execute(opts)
220
+ end
221
+
215
222
 
216
223
  end
@@ -11,6 +11,7 @@ class Morpheus::Cli::Clouds
11
11
  alias_subcommand :'get-type', :type
12
12
  register_subcommands :wiki, :update_wiki
13
13
  register_subcommands({:'update-logo' => :update_logo,:'update-dark-logo' => :update_dark_logo})
14
+ register_subcommands :list_affinity_groups, :get_affinity_group, :update_affinity_group, :add_affinity_group, :remove_affinity_group
14
15
  #register_subcommands :firewall_disable, :firewall_enable
15
16
  alias_subcommand :details, :get
16
17
  set_default_subcommand :list
@@ -1149,6 +1150,331 @@ EOT
1149
1150
  end
1150
1151
  end
1151
1152
 
1153
+ def list_affinity_groups(args)
1154
+ options = {}
1155
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
1156
+ opts.banner = subcommand_usage( "[cloud]")
1157
+ build_standard_list_options(opts, options)
1158
+ opts.footer = "List affinity groups for a cloud.\n" +
1159
+ "[cloud] is required. This is the name or id of an existing cloud."
1160
+ end
1161
+
1162
+ optparse.parse!(args)
1163
+ verify_args!(args:args, optparse:optparse, count:1)
1164
+ connect(options)
1165
+
1166
+ cloud = find_cloud_by_name_or_id(args[0])
1167
+ return 1 if cloud.nil?
1168
+ params = {}
1169
+ params.merge!(parse_list_options(options))
1170
+ json_response = @clouds_interface.list_affinity_groups(cloud['id'], params)
1171
+ render_response(json_response, options, 'affinityGroups') do
1172
+ affinity_groups = json_response['affinityGroups']
1173
+ print_h1 "Morpheus Cloud Affinity Groups: #{cloud['name']}", parse_list_subtitles(options)
1174
+ if affinity_groups.empty?
1175
+ print cyan,"No affinity groups found.",reset,"\n"
1176
+ else
1177
+ columns = {
1178
+ "ID" => 'id',
1179
+ "Name" => 'name',
1180
+ "Type" => lambda {|it| format_affinity_type(it['affinityType']) },
1181
+ "Resource Pool" => lambda {|it| it['pool'] ? (it['pool']['name'] || it['pool']['id']) : '' },
1182
+ "Visibility" => lambda {|it| it['visibility'].to_s.capitalize },
1183
+ # "Servers" => lambda {|it| it['serverCount'] },
1184
+ # "Source" => lambda {|it| it['source'] },
1185
+ }.upcase_keys!
1186
+ print as_pretty_table(affinity_groups, columns, options)
1187
+ print_results_pagination(json_response)
1188
+ end
1189
+ print reset,"\n"
1190
+ end
1191
+ return 0, nil
1192
+ end
1193
+
1194
+ def get_affinity_group(args)
1195
+ options = {}
1196
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
1197
+ opts.banner = subcommand_usage( "[cloud] [affinity group]")
1198
+ build_standard_get_options(opts, options)
1199
+ opts.footer = "Get details about a cloud affinity group.\n" +
1200
+ "[cloud] is required. This is the name or id of an existing cloud.\n" +
1201
+ "[affinity group] is required. This is the name or id of an existing affinity group."
1202
+ end
1203
+ optparse.parse!(args)
1204
+ verify_args!(args:args, optparse:optparse, count:2)
1205
+ connect(options)
1206
+
1207
+ cloud = find_cloud_by_name_or_id(args[0])
1208
+ return 1 if cloud.nil?
1209
+ # this finds the affinity group in the cloud api response, then fetches it by ID
1210
+ affinity_group = find_cloud_affinity_group_by_name_or_id(cloud['id'], args[1])
1211
+ if affinity_group.nil?
1212
+ print_red_alert "Affinity Group not found for '#{args[1]}'"
1213
+ exit 1
1214
+ end
1215
+
1216
+ params = {}
1217
+ params.merge!(parse_query_options(options))
1218
+ @clouds_interface.setopts(options)
1219
+ if options[:dry_run]
1220
+ print_dry_run @clouds_interface.dry.get_affinity_group(cloud['id'], affinity_group['id'], params)
1221
+ return
1222
+ end
1223
+ json_response = @clouds_interface.get_affinity_group(cloud['id'], affinity_group['id'], params)
1224
+ render_response(json_response, options, 'affinityGroup') do
1225
+ affinity_group = json_response['affinityGroup']
1226
+ print_h1 "Affinity Group Details", [], options
1227
+ columns = {
1228
+ "ID" => 'id',
1229
+ "Name" => 'name',
1230
+ "Type" => lambda {|it| format_affinity_type(it['affinityType']) },
1231
+ "Resource Pool" => lambda {|it| it['pool'] ? (it['pool']['name'] || it['pool']['id']) : '' },
1232
+ "Visibility" => lambda {|it| it['visibility'].to_s.capitalize },
1233
+ "Servers" => lambda {|it| it['servers'].size() },
1234
+ "Source" => lambda {|it| it['source'] },
1235
+ "Active" => lambda {|it| format_boolean(it['active']) },
1236
+ }
1237
+ print_description_list(columns, affinity_group)
1238
+ if affinity_group['servers'].size > 0
1239
+ print_h2 "Servers", options
1240
+ print as_pretty_table(affinity_group['servers'], [:id, :name], options)
1241
+ end
1242
+ print reset,"\n"
1243
+ end
1244
+ return 0, nil
1245
+
1246
+ end
1247
+
1248
+ def add_affinity_group(args)
1249
+ options = {}
1250
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
1251
+ opts.banner = subcommand_usage( "[cloud] [name] [options]")
1252
+ build_option_type_options(opts, options, add_cloud_affinity_group_option_types)
1253
+ # opts.on('--refresh [SECONDS]', String, "Refresh until execution is complete. Default interval is #{default_refresh_interval} seconds.") do |val|
1254
+ # options[:refresh_interval] = val.to_s.empty? ? default_refresh_interval : val.to_f
1255
+ # end
1256
+ # opts.on(nil, '--no-refresh', "Do not refresh" ) do
1257
+ # options[:no_refresh] = true
1258
+ # end
1259
+ build_standard_add_options(opts, options)
1260
+ opts.footer = "Add affinity group to a cloud.\n" +
1261
+ "[cloud] is required. This is the name or id of an existing cloud.\n" +
1262
+ "[name] is required. This is the name of the new affinity group."
1263
+ end
1264
+
1265
+ optparse.parse!(args)
1266
+ verify_args!(args:args, optparse:optparse, min:1, max:2)
1267
+ connect(options)
1268
+
1269
+ begin
1270
+ cloud = find_cloud_by_name_or_id(args[0])
1271
+ return 1 if cloud.nil?
1272
+ if args[1]
1273
+ options[:options]['name'] = args[1]
1274
+ end
1275
+ if options[:payload]
1276
+ payload = options[:payload]
1277
+ # support -O OPTION switch on top of --payload
1278
+ if options[:options]
1279
+ payload ||= {}
1280
+ payload.deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) })
1281
+ end
1282
+ else
1283
+ options[:params] ||= {}
1284
+ options[:params].merge!({:cloudId => cloud['id'],:zoneId => cloud['id']})
1285
+ option_types = add_cloud_affinity_group_option_types
1286
+ affinity_group = Morpheus::Cli::OptionTypes.prompt(option_types, options[:options], @api_client, options[:params])
1287
+
1288
+ # affinity_group_type = find_affinity_group_type_by_code(affinity_group['affinityGroupType'])
1289
+ # affinity_group['affinityGroupType'] = {id:affinity_group_type['id']}
1290
+
1291
+ # # affinity_group type options
1292
+ # unless affinity_group_type['optionTypes'].empty?
1293
+ # affinity_group.merge!(Morpheus::Cli::OptionTypes.prompt(affinity_group_type['optionTypes'], options[:options].deep_merge({:context_map => {'domain' => ''}, :checkbox_as_boolean => true}), @api_client, options[:params]))
1294
+ # end
1295
+
1296
+ # perms
1297
+ perms = prompt_permissions(options.merge({:for_affinity_group => true}), ['plans', 'groupDefaults'])
1298
+
1299
+ affinity_group['resourcePermissions'] = perms['resourcePermissions'] unless perms['resourcePermissions'].nil?
1300
+ affinity_group['tenants'] = perms['tenantPermissions'] unless perms['tenantPermissions'].nil?
1301
+ affinity_group['visibility'] = perms['resourcePool']['visibility'] if !perms['resourcePool'].nil? && !perms['resourcePool']['visibility'].nil?
1302
+
1303
+ payload = {'affinityGroup' => affinity_group}
1304
+ end
1305
+
1306
+ @clouds_interface.setopts(options)
1307
+ if options[:dry_run]
1308
+ print_dry_run @clouds_interface.dry.create_affinity_group(cloud['id'], payload)
1309
+ return
1310
+ end
1311
+ json_response = @clouds_interface.create_affinity_group(cloud['id'], payload)
1312
+ if options[:json]
1313
+ puts as_json(json_response)
1314
+ else
1315
+ if json_response['success']
1316
+ if json_response['msg'] == nil
1317
+ print_green_success "Adding affinity group to cloud #{cloud['name']}"
1318
+ else
1319
+ print_green_success json_response['msg']
1320
+ end
1321
+ execution_id = json_response['executionId']
1322
+ if !options[:no_refresh] && execution_id
1323
+ wait_for_execution_request(json_response['executionId'], options.merge({waiting_status:['new', 'pending', 'executing']}))
1324
+ end
1325
+ else
1326
+ print_red_alert "Failed to create cloud affinity group #{json_response['msg']}"
1327
+ end
1328
+ end
1329
+ return 0
1330
+ rescue RestClient::Exception => e
1331
+ print_rest_exception(e, options)
1332
+ exit 1
1333
+ end
1334
+ end
1335
+
1336
+ def update_affinity_group(args)
1337
+ options = {}
1338
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
1339
+ opts.banner = subcommand_usage( "[cloud] [affinity group] [options]")
1340
+ opts.on('--active [on|off]', String, "Enable affinity group") do |val|
1341
+ options[:active] = val.to_s == 'on' || val.to_s == 'true' || val.to_s == ''
1342
+ end
1343
+ add_perms_options(opts, options, ['groupDefaults'])
1344
+ build_standard_update_options(opts, options)
1345
+ opts.footer = "Update a cloud affinity group.\n" +
1346
+ "[cloud] is required. This is the name or id of an existing cloud.\n" +
1347
+ "[affinity group] is required. This is the name or id of an existing affinity group."
1348
+ end
1349
+
1350
+ optparse.parse!(args)
1351
+ if args.count != 2
1352
+ raise_command_error "wrong number of arguments, expected 2 and got (#{args.count}) #{args}\n#{optparse}"
1353
+ end
1354
+ connect(options)
1355
+
1356
+ begin
1357
+ cloud = find_cloud_by_name_or_id(args[0])
1358
+ return 1 if cloud.nil?
1359
+ affinity_group = find_cloud_affinity_group_by_name_or_id(cloud['id'], args[1])
1360
+ if affinity_group.nil?
1361
+ print_red_alert "Affinity Group not found by '#{args[1]}'"
1362
+ exit 1
1363
+ end
1364
+ payload = nil
1365
+ if options[:payload]
1366
+ payload = options[:payload]
1367
+ # support -O OPTION switch on top of everything
1368
+ if options[:options]
1369
+ payload.deep_merge!({'affinityGroup' => options[:options].reject {|k,v| k.is_a?(Symbol) }})
1370
+ end
1371
+ else
1372
+ payload = {'affinityGroup' => {}}
1373
+ payload['affinityGroup']['active'] = options[:active].nil? ? (Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'active', 'fieldLabel' => 'Active', 'type' => 'checkbox', 'description' => 'Active', 'defaultValue' => true}], options[:options], @api_client))['active'] == 'on' : options[:active]
1374
+
1375
+ perms = prompt_permissions(options.merge({:available_plans => namespace_service_plans}), affinity_group['owner']['id'] == current_user['accountId'] ? ['plans', 'groupDefaults'] : ['plans', 'groupDefaults', 'visibility', 'tenants'])
1376
+ perms_payload = {}
1377
+ perms_payload['resourcePermissions'] = perms['resourcePermissions'] if !perms['resourcePermissions'].nil?
1378
+ perms_payload['tenantPermissions'] = perms['tenantPermissions'] if !perms['tenantPermissions'].nil?
1379
+
1380
+ payload['affinityGroup']['permissions'] = perms_payload
1381
+ payload['affinityGroup']['visibility'] = perms['resourcePool']['visibility'] if !perms['resourcePool'].nil? && !perms['resourcePool']['visibility'].nil?
1382
+
1383
+ # support -O OPTION switch on top of everything
1384
+ if options[:options]
1385
+ payload.deep_merge!({'affinityGroup' => options[:options].reject {|k,v| k.is_a?(Symbol) }})
1386
+ end
1387
+
1388
+ if payload['affinityGroup'].nil? || payload['affinityGroup'].empty?
1389
+ raise_command_error "Specify at least one option to update.\n#{optparse}"
1390
+ end
1391
+ end
1392
+
1393
+ @clouds_interface.setopts(options)
1394
+ if options[:dry_run]
1395
+ print_dry_run @clouds_interface.dry.update_affinity_group(cloud['id'], affinity_group['id'], payload)
1396
+ return
1397
+ end
1398
+ json_response = @clouds_interface.update_affinity_group(cloud['id'], affinity_group['id'], payload)
1399
+ if options[:json]
1400
+ puts as_json(json_response)
1401
+ elsif !options[:quiet]
1402
+ affinity_group = json_response['affinityGroup']
1403
+ print_green_success "Updated affinity group #{affinity_group['name']}"
1404
+ #get_args = [cloud["id"], affinity_group["id"]] + (options[:remote] ? ["-r",options[:remote]] : [])
1405
+ #get_namespace(get_args)
1406
+ end
1407
+ return 0
1408
+ rescue RestClient::Exception => e
1409
+ print_rest_exception(e, options)
1410
+ exit 1
1411
+ end
1412
+ end
1413
+
1414
+ def remove_affinity_group(args)
1415
+ default_refresh_interval = 5
1416
+ params = {}
1417
+ options = {}
1418
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
1419
+ opts.banner = subcommand_usage("[cloud] [affinity group]")
1420
+ # opts.on( '-f', '--force', "Force Delete" ) do
1421
+ # params[:force] = 'on'
1422
+ # end
1423
+ # opts.on('--refresh [SECONDS]', String, "Refresh until execution is complete. Default interval is #{default_refresh_interval} seconds.") do |val|
1424
+ # options[:refresh_interval] = val.to_s.empty? ? default_refresh_interval : val.to_f
1425
+ # end
1426
+ # opts.on(nil, '--no-refresh', "Do not refresh" ) do
1427
+ # options[:no_refresh] = true
1428
+ # end
1429
+ build_standard_remove_options(opts, options)
1430
+ opts.footer = "Delete an affinity group from a cloud.\n" +
1431
+ "[cloud] is required. This is the name or id of an existing cloud.\n" +
1432
+ "[affinity group] is required. This is the name or id of an existing affinity group."
1433
+ end
1434
+ optparse.parse!(args)
1435
+ verify_args!(args:args, optparse:optparse, count:2)
1436
+ connect(options)
1437
+ params.merge!(parse_query_options(options))
1438
+
1439
+ cloud = find_cloud_by_name_or_id(args[0])
1440
+ return 1 if cloud.nil?
1441
+
1442
+ affinity_group_id = args[1]
1443
+ if affinity_group_id.empty?
1444
+ raise_command_error "missing required worker parameter"
1445
+ end
1446
+
1447
+ affinity_group = find_cloud_affinity_group_by_name_or_id(cloud['id'], affinity_group_id)
1448
+ if affinity_group.nil?
1449
+ print_red_alert "Affinity Group not found for '#{affinity_group_id}'"
1450
+ return 1
1451
+ end
1452
+ unless options[:yes] || ::Morpheus::Cli::OptionTypes::confirm("Are you sure you would like to remove the cloud affinity group '#{affinity_group['name'] || affinity_group['id']}'?", options)
1453
+ return 9, "aborted command"
1454
+ end
1455
+
1456
+ @clouds_interface.setopts(options)
1457
+ if options[:dry_run]
1458
+ print_dry_run @clouds_interface.dry.destroy_affinity_group(cloud['id'], affinity_group['id'], params)
1459
+ return
1460
+ end
1461
+ json_response = @clouds_interface.destroy_affinity_group(cloud['id'], affinity_group['id'], params)
1462
+ if options[:json]
1463
+ puts as_json(json_response)
1464
+ else
1465
+ if json_response['success']
1466
+ print_green_success "Removed affinity group #{affinity_group['name']}"
1467
+ execution_id = json_response['executionId']
1468
+ if !options[:no_refresh] && execution_id
1469
+ wait_for_execution_request(execution_id, options.merge({waiting_status:['new', 'pending', 'executing']}))
1470
+ end
1471
+ else
1472
+ print_red_alert "Failed to remove cloud affinity group #{json_response['msg']}"
1473
+ end
1474
+ end
1475
+ return 0, nil
1476
+ end
1477
+
1152
1478
  private
1153
1479
 
1154
1480
  def cloud_list_column_definitions(options)
@@ -1259,4 +1585,26 @@ EOT
1259
1585
  end
1260
1586
  end
1261
1587
 
1588
+ def find_cloud_affinity_group_by_name_or_id(cloud_id, val)
1589
+ if val.to_s =~ /\A\d{1,}\Z/
1590
+ @clouds_interface.get_affinity_group(cloud_id, val)['affinityGroup'] rescue nil
1591
+ else
1592
+ @clouds_interface.list_affinity_groups(cloud_id, {name: val})['affinityGroups'][0]
1593
+ end
1594
+ end
1595
+
1596
+ def add_cloud_affinity_group_option_types
1597
+ [
1598
+ {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true},
1599
+ {'fieldName' => 'affinityType', 'fieldLabel' => 'Type', 'type' => 'select', 'selectOptions' => [{'name' => 'Keep Separate', 'value' => 'KEEP_SEPARATE'}, {'name' => 'Keep Together', 'value' => 'KEEP_TOGETHER'}], 'description' => 'Choose affinity type.', 'required' => true, 'defaultValue' => 'KEEP_SEPARATE'},
1600
+ {'fieldName' => 'active', 'fieldLabel' => 'Active', 'type' => 'checkbox', 'defaultValue' => true},
1601
+ {'fieldName' => 'pool.id', 'fieldLabel' => 'Cluster', 'type' => 'select', 'optionSourceType' => 'vmware', 'optionSource' => 'vmwareZonePoolClusters', 'description' => 'Select cluster for the affinity group.', 'required' => true},
1602
+ {'fieldName' => 'servers', 'fieldLabel' => 'Server', 'type' => 'multiSelect', 'optionSource' => 'searchServers', 'description' => 'Select servers to be in the affinity group.'},
1603
+ ]
1604
+ end
1605
+
1606
+ def format_affinity_type(affinity_type)
1607
+ affinity_type == "KEEP_SEPARATE" ? "Keep Separate" : "Keep Together"
1608
+ end
1609
+
1262
1610
  end
@@ -28,6 +28,7 @@ class Morpheus::Cli::Clusters
28
28
  register_subcommands :refresh
29
29
  register_subcommands :list_replicasets, :list_daemonsets, :list_endpoints, :list_ingresses, :list_policies, :list_volumes, :list_volume_claims, :list_config_maps, :list_secrets
30
30
  register_subcommands :get_pod, :get_deployment, :get_replicaset, :get_daemonset, :get_endpoint, :get_ingress, :get_policy, :get_volume_claim, :get_volume, :get_config_map, :get_secret, :get_stateful_set, :get_job, :get_service
31
+ register_subcommands :list_affinity_groups, :get_affinity_group, :update_affinity_group, :add_affinity_group, :remove_affinity_group
31
32
 
32
33
  def connect(opts)
33
34
  @api_client = establish_remote_appliance_connection(opts)
@@ -1379,7 +1380,12 @@ class Morpheus::Cli::Clusters
1379
1380
  (['provisionType.vmware.host', 'provisionType.scvmm.host'].include?(type['code']) && cloud['config']['hideHostSelection'] == 'on') || # should this be truthy?
1380
1381
  (type['fieldContext'] == 'instance.networkDomain' && type['fieldName'] == 'id')
1381
1382
  } rescue [])
1382
-
1383
+ # strip server context
1384
+ option_type_list.each do |option_type|
1385
+ if option_type['fieldContext'] == 'server' || option_type['fieldContext'] == 'domain'
1386
+ option_type['fieldContext'] = nil
1387
+ end
1388
+ end
1383
1389
  # remove metadata option_type , prompt manually for that field 'tags' instead of 'metadata'
1384
1390
  #metadata_option_type = option_type_list.find {|type| type['fieldName'] == 'metadata' }
1385
1391
  metadata_option_type = cluster_type['optionTypes'].find {|type| type['fieldName'] == 'metadata' }
@@ -3269,6 +3275,337 @@ class Morpheus::Cli::Clusters
3269
3275
  end
3270
3276
  end
3271
3277
 
3278
+ def list_affinity_groups(args)
3279
+ options = {}
3280
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
3281
+ opts.banner = subcommand_usage( "[cluster]")
3282
+ build_standard_list_options(opts, options)
3283
+ opts.footer = "List affinity groups for a cluster.\n" +
3284
+ "[cluster] is required. This is the name or id of an existing cluster."
3285
+ end
3286
+
3287
+ optparse.parse!(args)
3288
+ verify_args!(args:args, optparse:optparse, count:1)
3289
+ connect(options)
3290
+
3291
+ cluster = find_cluster_by_name_or_id(args[0])
3292
+ return 1 if cluster.nil?
3293
+
3294
+ params = {}
3295
+ params.merge!(parse_list_options(options))
3296
+ @clusters_interface.setopts(options)
3297
+ if options[:dry_run]
3298
+ print_dry_run @clusters_interface.dry.list_affinity_groups(cluster['id'], params)
3299
+ return
3300
+ end
3301
+ json_response = @clusters_interface.list_affinity_groups(cluster['id'], params)
3302
+ render_response(json_response, options, 'affinityGroups') do
3303
+ affinity_groups = json_response['affinityGroups']
3304
+ print_h1 "Morpheus Cluster Affinity Groups: #{cluster['name']}", parse_list_subtitles(options), options
3305
+ if affinity_groups.empty?
3306
+ print cyan,"No affinity groups found.",reset,"\n"
3307
+ else
3308
+ columns = {
3309
+ "ID" => 'id',
3310
+ "Name" => 'name',
3311
+ "Type" => lambda {|it| format_affinity_type(it['affinityType']) },
3312
+ "Resource Pool" => lambda {|it| it['pool'] ? (it['pool']['name'] || it['pool']['id']) : '' },
3313
+ "Visibility" => lambda {|it| it['visibility'].to_s.capitalize },
3314
+ "Servers" => lambda {|it| it['servers'].size() },
3315
+ # "Source" => lambda {|it| it['source'] },
3316
+ }.upcase_keys!
3317
+ print as_pretty_table(affinity_groups, columns, options)
3318
+ print_results_pagination(json_response)
3319
+ end
3320
+ print reset,"\n"
3321
+ end
3322
+ return 0, nil
3323
+ end
3324
+
3325
+ def get_affinity_group(args)
3326
+ options = {}
3327
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
3328
+ opts.banner = subcommand_usage( "[cluster] [affinity group]")
3329
+ build_standard_get_options(opts, options)
3330
+ opts.footer = "Get details about a cluster affinity group.\n" +
3331
+ "[cluster] is required. This is the name or id of an existing cluster.\n" +
3332
+ "[affinity group] is required. This is the name or id of an existing affinity group."
3333
+ end
3334
+ optparse.parse!(args)
3335
+ verify_args!(args:args, optparse:optparse, count:2)
3336
+ connect(options)
3337
+
3338
+ cluster = find_cluster_by_name_or_id(args[0])
3339
+ return 1 if cluster.nil?
3340
+
3341
+ # this finds the affinity group in the cluster api response, then fetches it by ID
3342
+ affinity_group = find_cluster_affinity_group_by_name_or_id(cluster['id'], args[1])
3343
+ if affinity_group.nil?
3344
+ print_red_alert "Affinity Group not found for '#{args[1]}'"
3345
+ exit 1
3346
+ end
3347
+
3348
+ params = {}
3349
+ params.merge!(parse_query_options(options))
3350
+ @clusters_interface.setopts(options)
3351
+ if options[:dry_run]
3352
+ print_dry_run @clusters_interface.dry.get_affinity_group(cluster['id'], affinity_group['id'], params)
3353
+ return
3354
+ end
3355
+ json_response = @clusters_interface.get_affinity_group(cluster['id'], affinity_group['id'], params)
3356
+ render_response(json_response, options, 'affinityGroup') do
3357
+ affinity_group = json_response['affinityGroup']
3358
+ print_h1 "Affinity Group Details", [], options
3359
+ columns = {
3360
+ "ID" => 'id',
3361
+ "Name" => 'name',
3362
+ "Type" => lambda {|it| format_affinity_type(it['affinityType']) },
3363
+ "Resource Pool" => lambda {|it| it['pool'] ? (it['pool']['name'] || it['pool']['id']) : '' },
3364
+ "Visibility" => lambda {|it| it['visibility'].to_s.capitalize },
3365
+ "Servers" => lambda {|it| it['servers'].size() },
3366
+ "Source" => lambda {|it| it['source'] },
3367
+ "Active" => lambda {|it| format_boolean(it['active']) },
3368
+ }
3369
+ print_description_list(columns, affinity_group)
3370
+ if affinity_group['servers'].size > 0
3371
+ print_h2 "Servers", options
3372
+ print as_pretty_table(affinity_group['servers'], [:id, :name], options)
3373
+ end
3374
+ print reset,"\n"
3375
+ end
3376
+ return 0, nil
3377
+ end
3378
+
3379
+ def add_affinity_group(args)
3380
+ options = {}
3381
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
3382
+ opts.banner = subcommand_usage( "[cluster] [name] [options]")
3383
+ build_option_type_options(opts, options, add_affinity_group_option_types)
3384
+ # opts.on('--refresh [SECONDS]', String, "Refresh until execution is complete. Default interval is #{default_refresh_interval} seconds.") do |val|
3385
+ # options[:refresh_interval] = val.to_s.empty? ? default_refresh_interval : val.to_f
3386
+ # end
3387
+ # opts.on(nil, '--no-refresh', "Do not refresh" ) do
3388
+ # options[:no_refresh] = true
3389
+ # end
3390
+ build_standard_add_options(opts, options)
3391
+ opts.footer = "Add affinity group to a cluster.\n" +
3392
+ "[cluster] is required. This is the name or id of an existing cluster.\n" +
3393
+ "[name] is required. This is the name of the new affinity group."
3394
+ end
3395
+
3396
+ optparse.parse!(args)
3397
+ verify_args!(args:args, optparse:optparse, min:1, max:2)
3398
+ connect(options)
3399
+
3400
+ begin
3401
+ cluster = find_cluster_by_name_or_id(args[0])
3402
+ return 1 if cluster.nil?
3403
+ if args[1]
3404
+ options[:options]['name'] = args[1]
3405
+ end
3406
+ if options[:payload]
3407
+ payload = options[:payload]
3408
+ # support -O OPTION switch on top of --payload
3409
+ if options[:options]
3410
+ payload ||= {}
3411
+ payload.deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) })
3412
+ end
3413
+ else
3414
+ options[:params] ||= {}
3415
+ options[:params].merge!({:serverGroupId => cluster['id']})
3416
+ option_types = add_affinity_group_option_types
3417
+ affinity_group = Morpheus::Cli::OptionTypes.prompt(option_types, options[:options], @api_client, options[:params])
3418
+
3419
+ # affinity_group_type = find_affinity_group_type_by_code(affinity_group['affinityGroupType'])
3420
+ # affinity_group['affinityGroupType'] = {id:affinity_group_type['id']}
3421
+
3422
+ # # affinity_group type options
3423
+ # unless affinity_group_type['optionTypes'].empty?
3424
+ # affinity_group.merge!(Morpheus::Cli::OptionTypes.prompt(affinity_group_type['optionTypes'], options[:options].deep_merge({:context_map => {'domain' => ''}, :checkbox_as_boolean => true}), @api_client, options[:params]))
3425
+ # end
3426
+
3427
+ # perms
3428
+ perms = prompt_permissions(options.merge({:for_affinity_group => true}), ['groupDefaults'])
3429
+
3430
+ affinity_group['resourcePermissions'] = perms['resourcePermissions'] unless perms['resourcePermissions'].nil?
3431
+ affinity_group['tenants'] = perms['tenantPermissions'] unless perms['tenantPermissions'].nil?
3432
+ affinity_group['visibility'] = perms['resourcePool']['visibility'] if !perms['resourcePool'].nil? && !perms['resourcePool']['visibility'].nil?
3433
+
3434
+ payload = {'affinityGroup' => affinity_group}
3435
+ end
3436
+
3437
+ @clusters_interface.setopts(options)
3438
+ if options[:dry_run]
3439
+ print_dry_run @clusters_interface.dry.create_affinity_group(cluster['id'], payload)
3440
+ return
3441
+ end
3442
+ json_response = @clusters_interface.create_affinity_group(cluster['id'], payload)
3443
+ if options[:json]
3444
+ puts as_json(json_response)
3445
+ else
3446
+ if json_response['success']
3447
+ if json_response['msg'] == nil
3448
+ print_green_success "Adding affinity group to cluster #{cluster['name']}"
3449
+ else
3450
+ print_green_success json_response['msg']
3451
+ end
3452
+ execution_id = json_response['executionId']
3453
+ if !options[:no_refresh] && execution_id
3454
+ wait_for_execution_request(json_response['executionId'], options.merge({waiting_status:['new', 'pending', 'executing']}))
3455
+ end
3456
+ else
3457
+ print_red_alert "Failed to create cluster affinity group #{json_response['msg']}"
3458
+ end
3459
+ end
3460
+ return 0
3461
+ rescue RestClient::Exception => e
3462
+ print_rest_exception(e, options)
3463
+ exit 1
3464
+ end
3465
+ end
3466
+
3467
+ def update_affinity_group(args)
3468
+ options = {}
3469
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
3470
+ opts.banner = subcommand_usage( "[cluster] [affinity group] [options]")
3471
+ opts.on('--active [on|off]', String, "Enable affinity group") do |val|
3472
+ options[:active] = val.to_s == 'on' || val.to_s == 'true' || val.to_s == ''
3473
+ end
3474
+ # add_perms_options(opts, options, ['groupDefaults'])
3475
+ build_standard_update_options(opts, options)
3476
+ opts.footer = "Update a cluster affinity group.\n" +
3477
+ "[cluster] is required. This is the name or id of an existing cluster.\n" +
3478
+ "[affinity group] is required. This is the name or id of an existing affinity group."
3479
+ end
3480
+
3481
+ optparse.parse!(args)
3482
+ if args.count != 2
3483
+ raise_command_error "wrong number of arguments, expected 2 and got (#{args.count}) #{args}\n#{optparse}"
3484
+ end
3485
+ connect(options)
3486
+
3487
+ begin
3488
+ cluster = find_cluster_by_name_or_id(args[0])
3489
+ return 1 if cluster.nil?
3490
+ affinity_group = find_cluster_affinity_group_by_name_or_id(cluster['id'], args[1])
3491
+ if affinity_group.nil?
3492
+ print_red_alert "Affinity Group not found by '#{args[1]}'"
3493
+ exit 1
3494
+ end
3495
+ payload = nil
3496
+ if options[:payload]
3497
+ payload = options[:payload]
3498
+ # support -O OPTION switch on top of everything
3499
+ if options[:options]
3500
+ payload.deep_merge!({'affinityGroup' => options[:options].reject {|k,v| k.is_a?(Symbol) }})
3501
+ end
3502
+ else
3503
+ payload = {'affinityGroup' => {}}
3504
+ payload['affinityGroup']['active'] = options[:active].nil? ? (Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'active', 'fieldLabel' => 'Active', 'type' => 'checkbox', 'description' => 'Active', 'defaultValue' => true}], options[:options], @api_client))['active'] == 'on' : options[:active]
3505
+
3506
+ perms = prompt_permissions(options.merge({:available_plans => namespace_service_plans}), affinity_group['owner']['id'] == current_user['accountId'] ? ['plans', 'groupDefaults'] : ['plans', 'groupDefaults', 'visibility', 'tenants'])
3507
+ perms_payload = {}
3508
+ perms_payload['resourcePermissions'] = perms['resourcePermissions'] if !perms['resourcePermissions'].nil?
3509
+ perms_payload['tenantPermissions'] = perms['tenantPermissions'] if !perms['tenantPermissions'].nil?
3510
+
3511
+ payload['affinityGroup']['permissions'] = perms_payload
3512
+ payload['affinityGroup']['visibility'] = perms['resourcePool']['visibility'] if !perms['resourcePool'].nil? && !perms['resourcePool']['visibility'].nil?
3513
+
3514
+ # support -O OPTION switch on top of everything
3515
+ if options[:options]
3516
+ payload.deep_merge!({'affinityGroup' => options[:options].reject {|k,v| k.is_a?(Symbol) }})
3517
+ end
3518
+
3519
+ if payload['affinityGroup'].nil? || payload['affinityGroup'].empty?
3520
+ raise_command_error "Specify at least one option to update.\n#{optparse}"
3521
+ end
3522
+ end
3523
+
3524
+ @clusters_interface.setopts(options)
3525
+ if options[:dry_run]
3526
+ print_dry_run @clusters_interface.dry.update_affinity_group(cluster['id'], affinity_group['id'], payload)
3527
+ return
3528
+ end
3529
+ json_response = @clusters_interface.update_affinity_group(cluster['id'], affinity_group['id'], payload)
3530
+ if options[:json]
3531
+ puts as_json(json_response)
3532
+ elsif !options[:quiet]
3533
+ affinity_group = json_response['affinityGroup']
3534
+ print_green_success "Updated affinity group #{affinity_group['name']}"
3535
+ #get_args = [cluster["id"], affinity_group["id"]] + (options[:remote] ? ["-r",options[:remote]] : [])
3536
+ #get_namespace(get_args)
3537
+ end
3538
+ return 0
3539
+ rescue RestClient::Exception => e
3540
+ print_rest_exception(e, options)
3541
+ exit 1
3542
+ end
3543
+ end
3544
+
3545
+ def remove_affinity_group(args)
3546
+ default_refresh_interval = 5
3547
+ params = {}
3548
+ options = {}
3549
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
3550
+ opts.banner = subcommand_usage("[cluster] [affinity group]")
3551
+ # opts.on( '-f', '--force', "Force Delete" ) do
3552
+ # params[:force] = 'on'
3553
+ # end
3554
+ # opts.on('--refresh [SECONDS]', String, "Refresh until execution is complete. Default interval is #{default_refresh_interval} seconds.") do |val|
3555
+ # options[:refresh_interval] = val.to_s.empty? ? default_refresh_interval : val.to_f
3556
+ # end
3557
+ # opts.on(nil, '--no-refresh', "Do not refresh" ) do
3558
+ # options[:no_refresh] = true
3559
+ # end
3560
+ build_standard_remove_options(opts, options)
3561
+ opts.footer = "Delete an affinity group from a cluster.\n" +
3562
+ "[cluster] is required. This is the name or id of an existing cluster.\n" +
3563
+ "[affinity group] is required. This is the name or id of an existing affinity group."
3564
+ end
3565
+ optparse.parse!(args)
3566
+ verify_args!(args:args, optparse:optparse, count:2)
3567
+ connect(options)
3568
+ params.merge!(parse_query_options(options))
3569
+
3570
+ cluster = find_cluster_by_name_or_id(args[0])
3571
+ return 1 if cluster.nil?
3572
+
3573
+ affinity_group_id = args[1]
3574
+ if affinity_group_id.empty?
3575
+ raise_command_error "missing required worker parameter"
3576
+ end
3577
+
3578
+ affinity_group = find_cluster_affinity_group_by_name_or_id(cluster['id'], affinity_group_id)
3579
+ if affinity_group.nil?
3580
+ print_red_alert "Affinity Group not found for '#{affinity_group_id}'"
3581
+ return 1
3582
+ end
3583
+ unless options[:yes] || ::Morpheus::Cli::OptionTypes::confirm("Are you sure you would like to remove the cluster affinity group '#{affinity_group['name'] || affinity_group['id']}'?", options)
3584
+ return 9, "aborted command"
3585
+ end
3586
+
3587
+ @clusters_interface.setopts(options)
3588
+ if options[:dry_run]
3589
+ print_dry_run @clusters_interface.dry.destroy_affinity_group(cluster['id'], affinity_group['id'], params)
3590
+ return
3591
+ end
3592
+ json_response = @clusters_interface.destroy_affinity_group(cluster['id'], affinity_group['id'], params)
3593
+ if options[:json]
3594
+ puts as_json(json_response)
3595
+ else
3596
+ if json_response['success']
3597
+ print_green_success "Removed affinity group #{affinity_group['name']}"
3598
+ execution_id = json_response['executionId']
3599
+ if !options[:no_refresh] && execution_id
3600
+ wait_for_execution_request(execution_id, options.merge({waiting_status:['new', 'pending', 'executing']}))
3601
+ end
3602
+ else
3603
+ print_red_alert "Failed to remove cluster affinity_group #{json_response['msg']}"
3604
+ end
3605
+ end
3606
+ return 0, nil
3607
+ end
3608
+
3272
3609
  def api_config(args)
3273
3610
  options = {}
3274
3611
  optparse = Morpheus::Cli::OptionParser.new do |opts|
@@ -4866,4 +5203,27 @@ class Morpheus::Cli::Clusters
4866
5203
  connect(options)
4867
5204
  _list_container_groups(args, options, resource_type)
4868
5205
  end
5206
+
5207
+ def find_cluster_affinity_group_by_name_or_id(cluster_id, val)
5208
+ if val.to_s =~ /\A\d{1,}\Z/
5209
+ @clusters_interface.get_affinity_group(cluster_id, val)['affinityGroup'] rescue nil
5210
+ else
5211
+ @clusters_interface.list_affinity_groups(cluster_id, {name: val})['affinityGroups'][0]
5212
+ end
5213
+ end
5214
+
5215
+ def add_affinity_group_option_types
5216
+ [
5217
+ {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true},
5218
+ {'fieldName' => 'affinityType', 'fieldLabel' => 'Type', 'type' => 'select', 'selectOptions' => [{'name' => 'Keep Separate', 'value' => 'KEEP_SEPARATE'}, {'name' => 'Keep Together', 'value' => 'KEEP_TOGETHER'}], 'description' => 'Choose affinity type.', 'required' => true, 'defaultValue' => 'KEEP_SEPARATE'},
5219
+ {'fieldName' => 'active', 'fieldLabel' => 'Active', 'type' => 'checkbox', 'defaultValue' => true},
5220
+ # {'fieldName' => 'pool.id', 'fieldLabel' => 'Cluster', 'type' => 'select', 'optionSourceType' => 'vmware', 'optionSource' => 'vmwareZonePoolClusters', 'description' => 'Select cluster for the affinity group.', 'required' => true},
5221
+ {'fieldName' => 'servers', 'fieldLabel' => 'Server', 'type' => 'multiSelect', 'optionSource' => 'searchServers', 'description' => 'Select servers to be in the affinity group.'},
5222
+ ]
5223
+ end
5224
+
5225
+ def format_affinity_type(affinity_type)
5226
+ affinity_type == "KEEP_SEPARATE" ? "Keep Separate" : "Keep Together"
5227
+ end
5228
+
4869
5229
  end
@@ -3,6 +3,7 @@ require 'morpheus/cli/cli_command'
3
3
  class Morpheus::Cli::Hosts
4
4
  include Morpheus::Cli::CliCommand
5
5
  include Morpheus::Cli::AccountsHelper
6
+ include Morpheus::Cli::ProcessesHelper
6
7
  include Morpheus::Cli::ProvisioningHelper
7
8
  include Morpheus::Cli::LogsHelper
8
9
  set_command_name :hosts
@@ -13,7 +14,8 @@ class Morpheus::Cli::Hosts
13
14
  {:exec => :execution_request},
14
15
  :wiki, :update_wiki,
15
16
  :maintenance, :leave_maintenance, :placement,
16
- :list_devices, :assign_device, :detach_device, :attach_device
17
+ :list_devices, :assign_device, :detach_device, :attach_device,
18
+ :snapshot
17
19
  alias_subcommand :details, :get
18
20
  set_default_subcommand :list
19
21
 
@@ -538,8 +540,9 @@ class Morpheus::Cli::Hosts
538
540
  "Nodes" => lambda {|it| it['containers'] ? it['containers'].size : 0 },
539
541
  # "Status" => lambda {|it| format_server_status(it) },
540
542
  # "Power" => lambda {|it| format_server_power_state(it) },
541
- "Status" => lambda {|it| format_server_status_friendly(it) }, # combo
542
- "Managed" => lambda {|it| it['computeServerType'] ? it['computeServerType']['managed'] : ''}
543
+ "Managed" => lambda {|it| it['computeServerType'] ? it['computeServerType']['managed'] : ''},
544
+ "Instance" => lambda {|it| it['instance'] ? it['instance']['name'] : ''},
545
+ "Status" => lambda {|it| format_server_status_friendly(it) } # combo
543
546
  }
544
547
  server_columns.delete("Hostname") if server['hostname'].to_s.empty? || server['hostname'] == server['name']
545
548
  server_columns.delete("IP") if server['externalIp'].to_s.empty?
@@ -548,6 +551,7 @@ class Morpheus::Cli::Hosts
548
551
  server_columns.delete("Cost") if server['hourlyCost'].to_f == 0
549
552
  server_columns.delete("Price") if server['hourlyPrice'].to_f == 0 || server['hourlyPrice'] == server['hourlyCost']
550
553
  server_columns.delete("Labels") if server['labels'].nil? || server['labels'].empty?
554
+ server_columns.delete("Instance") if server['instance'].nil?
551
555
 
552
556
  print_description_list(server_columns, server)
553
557
 
@@ -2043,13 +2047,18 @@ EOT
2043
2047
  def snapshots(args)
2044
2048
  options = {}
2045
2049
  optparse = Morpheus::Cli::OptionParser.new do |opts|
2046
- opts.banner = subcommand_usage("[host]")
2050
+ opts.banner = subcommand_usage("[host] [snapshot]")
2047
2051
  # no pagination yet
2048
2052
  # build_standard_list_options(opts, options)
2049
- build_standard_get_options(opts, options)
2053
+ build_standard_list_options(opts, options, [:details])
2054
+ opts.footer = <<-EOT
2055
+ List snapshots for a host.
2056
+ [host] is required. This is the name or id of a host.
2057
+ [snapshot] is optional. This is the name or id of a snapshot.
2058
+ EOT
2050
2059
  end
2051
2060
  optparse.parse!(args)
2052
- verify_args!(args:args, optparse:optparse, count:1)
2061
+ verify_args!(args:args, optparse:optparse, min:1)
2053
2062
  connect(options)
2054
2063
  begin
2055
2064
  server = find_host_by_name_or_id(args[0])
@@ -2061,25 +2070,52 @@ EOT
2061
2070
  return
2062
2071
  end
2063
2072
  json_response = @servers_interface.snapshots(server['id'], params)
2064
- snapshots = json_response['snapshots']
2073
+ snapshots = json_response['snapshots']
2074
+ # [snapshots] is done with post api filtering by id or name or externalId
2075
+ if args[1]
2076
+ if args[1] =~ /\A\d{1,}\Z/
2077
+ snapshots = snapshots.select {|it| it['id'].to_s == args[1] }
2078
+ else
2079
+ # match beginning of name of externalId
2080
+ snapshots = snapshots.select {|it| it['name'].to_s.index(args[1]) == 0 || it['externalId'].to_s.index(args[1]) == 0 }
2081
+ end
2082
+ json_response['snapshots'] = snapshots # update response for -j filtering too
2083
+ end
2065
2084
  render_response(json_response, options, 'snapshots') do
2066
2085
  print_h1 "Snapshots: #{server['name']}", [], options
2067
2086
  if snapshots.empty?
2068
- print cyan,"No snapshots found",reset,"\n"
2087
+ if args[1]
2088
+ print cyan,"No snapshots found for '#{args[1]}'",reset,"\n"
2089
+ elsif
2090
+ print cyan,"No snapshots found",reset,"\n"
2091
+ end
2092
+ print reset, "\n"
2069
2093
  else
2070
- snapshot_column_definitions = {
2071
- "ID" => lambda {|it| it['id'] },
2072
- "Name" => lambda {|it| it['name'] },
2073
- "Description" => lambda {|it| it['description'] },
2074
- # "Type" => lambda {|it| it['snapshotType'] },
2075
- "Date Created" => lambda {|it| format_local_dt(it['snapshotCreated']) },
2076
- "Status" => lambda {|it| format_snapshot_status(it) }
2077
- }
2078
- print cyan
2079
- print as_pretty_table(snapshots, snapshot_column_definitions.upcase_keys!, options)
2080
- print_results_pagination({size: snapshots.size, total: snapshots.size})
2094
+ if options[:details]
2095
+ # this actually makes a request for each one here so don't go crazy...
2096
+ if snapshots.size > 3
2097
+ print cyan, "Showing first 3 snapshots. Use the ID to get more details.", reset, "\n"
2098
+ snapshots = snapshots.first(3)
2099
+ end
2100
+ snapshots.each do |snapshot|
2101
+ Morpheus::Cli::Snapshots.new.handle(["get", snapshot['id']] + (options[:remote] ? ["-r",options[:remote]] : []))
2102
+ end
2103
+ else
2104
+ # Snapshots List
2105
+ snapshot_column_definitions = {
2106
+ "ID" => lambda {|it| it['id'] },
2107
+ "Name" => lambda {|it| it['name'] },
2108
+ "Description" => lambda {|it| it['description'] },
2109
+ # "Type" => lambda {|it| it['snapshotType'] },
2110
+ "Date Created" => lambda {|it| format_local_dt(it['snapshotCreated']) },
2111
+ "Status" => lambda {|it| format_snapshot_status(it) }
2112
+ }
2113
+ print cyan
2114
+ print as_pretty_table(snapshots, snapshot_column_definitions.upcase_keys!, options)
2115
+ print_results_pagination({size: snapshots.size, total: snapshots.size})
2116
+ print reset, "\n"
2117
+ end
2081
2118
  end
2082
- print reset, "\n"
2083
2119
  end
2084
2120
  return 0
2085
2121
  rescue RestClient::Exception => e
@@ -2442,6 +2478,94 @@ EOT
2442
2478
  return 0, nil
2443
2479
  end
2444
2480
 
2481
+ def snapshot(args)
2482
+ options = {}
2483
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
2484
+ opts.banner = subcommand_usage("[host]")
2485
+ opts.on( '--name VALUE', String, "Snapshot Name. Default is \"{name}.{timestamp}\"" ) do |val|
2486
+ options[:options]['name'] = val
2487
+ end
2488
+ opts.on( '--description VALUE', String, "Snapshot Description." ) do |val|
2489
+ options[:options]['description'] = val
2490
+ end
2491
+ opts.on('--memory-snapshot [on|off]', String, "Memory Snapshot? Whether to include the memory state in the snapshot.") do |val|
2492
+ options[:options]['memorySnapshot'] = val.to_s == '' || val.to_s == 'on' || val.to_s == 'true'
2493
+ end
2494
+ opts.on('--for-export [on|off]', String, "For Export? Indicates the snapshot is intended for export to storage.") do |val|
2495
+ options[:options]['forExport'] = val.to_s == '' || val.to_s == 'on' || val.to_s == 'true'
2496
+ end
2497
+ opts.on('--refresh [SECONDS]', String, "Refresh until execution is complete. Default interval is #{default_refresh_interval} seconds.") do |val|
2498
+ options[:refresh_interval] = val.to_s.empty? ? default_refresh_interval : val.to_f
2499
+ end
2500
+ opts.on(nil, '--no-refresh', "Do not refresh" ) do
2501
+ options[:no_refresh] = true
2502
+ end
2503
+ build_standard_add_options(opts, options, [:auto_confirm])
2504
+ opts.footer = <<-EOT
2505
+ Create a snapshot for a host.
2506
+ [host] is required. This is the name or id of a host
2507
+ EOT
2508
+ end
2509
+ optparse.parse!(args)
2510
+ verify_args!(args:args, optparse:optparse, count:1)
2511
+ connect(options)
2512
+ server = find_host_by_name_or_id(args[0])
2513
+ payload = {}
2514
+ if options[:payload]
2515
+ payload = options[:payload]
2516
+ payload.deep_merge!({'snapshot' => parse_passed_options(options)})
2517
+ else
2518
+ payload.deep_merge!({'snapshot' => parse_passed_options(options)})
2519
+ # prompt for name and description
2520
+ name = prompt_value({'fieldName' => 'name', 'type' => 'text', 'fieldLabel' => 'Snapshot Name', 'description' => "Snapshot Name. Default is \"{name}.{timestamp}\""}, options)
2521
+ payload['snapshot']['name'] = name if !name.to_s.empty?
2522
+ description = prompt_value({'fieldName' => 'description', 'type' => 'text', 'fieldLabel' => 'Description', 'description' => "Snapshot Description."}, options)
2523
+ payload['snapshot']['description'] = description if !description.to_s.empty?
2524
+ # need to GET provision type for some settings...
2525
+ provision_type = nil
2526
+ begin
2527
+ provision_type_id = server['computeServerType']['provisionTypeId'] rescue nil
2528
+ if provision_type_id
2529
+ provision_type = @provision_types_interface.get(provision_type_id)['provisionType']
2530
+ end
2531
+ rescue => ex
2532
+ Morpheus::Logging::DarkPrinter.puts "Failed to load provision type!" if Morpheus::Logging.debug?
2533
+ end
2534
+ if provision_type && provision_type['hasMemorySnapshots']
2535
+ # prompt for memorySnapshot
2536
+ memory_snapshot = prompt_value({'fieldName' => 'memorySnapshot', 'type' => 'checkbox', 'fieldLabel' => 'Memory Snapshot?', 'description' => "Snapshot Description."}, options)
2537
+ payload['snapshot']['memorySnapshot'] = memory_snapshot if !memory_snapshot.to_s.empty?
2538
+ end
2539
+ # convert "on" and "off" to true/false
2540
+ payload.booleanize!
2541
+ end
2542
+ unless options[:yes] || ::Morpheus::Cli::OptionTypes::confirm("Are you sure you would like to snapshot the host '#{server['name']}'?", options)
2543
+ exit 1
2544
+ end
2545
+ @servers_interface.setopts(options)
2546
+ if options[:dry_run]
2547
+ print_dry_run @servers_interface.dry.snapshot(server['id'], payload)
2548
+ return
2549
+ end
2550
+ json_response = @servers_interface.snapshot(server['id'], payload)
2551
+ render_response(json_response, options) do
2552
+ print_green_success "Snapshot initiated."
2553
+ process_id = json_response['processIds'][0] rescue nil
2554
+ if process_id
2555
+ unless options[:no_refresh]
2556
+ process = wait_for_process_execution(process_id, options)
2557
+ snapshot_id = process['resultId']
2558
+ if snapshot_id
2559
+ Morpheus::Cli::Snapshots.new.handle(["get", snapshot_id] + (options[:remote] ? ["-r",options[:remote]] : []))
2560
+ end
2561
+ end
2562
+ else
2563
+ # puts "No process returned"
2564
+ end
2565
+ end
2566
+ return 0, nil
2567
+ end
2568
+
2445
2569
  ## Server Devices
2446
2570
 
2447
2571
  def list_devices(args)
@@ -2915,12 +2915,18 @@ class Morpheus::Cli::Instances
2915
2915
  options = {}
2916
2916
  optparse = Morpheus::Cli::OptionParser.new do |opts|
2917
2917
  opts.banner = subcommand_usage("[instance]")
2918
- opts.on( '--name VALUE', String, "Snapshot Name. Default is server name + timestamp" ) do |val|
2918
+ opts.on( '--name VALUE', String, "Snapshot Name. Default is \"{name}.{timestamp}\"" ) do |val|
2919
2919
  options[:options]['name'] = val
2920
2920
  end
2921
2921
  opts.on( '--description VALUE', String, "Snapshot Description." ) do |val|
2922
2922
  options[:options]['description'] = val
2923
2923
  end
2924
+ opts.on('--memory-snapshot [on|off]', String, "Memory Snapshot? Whether to include the memory state in the snapshot.") do |val|
2925
+ options[:options]['memorySnapshot'] = val.to_s == '' || val.to_s == 'on' || val.to_s == 'true'
2926
+ end
2927
+ opts.on('--for-export [on|off]', String, "For Export? Indicates the snapshot is intended for export to storage.") do |val|
2928
+ options[:options]['forExport'] = val.to_s == '' || val.to_s == 'on' || val.to_s == 'true'
2929
+ end
2924
2930
  opts.on('--refresh [SECONDS]', String, "Refresh until execution is complete. Default interval is #{default_refresh_interval} seconds.") do |val|
2925
2931
  options[:refresh_interval] = val.to_s.empty? ? default_refresh_interval : val.to_f
2926
2932
  end
@@ -2937,15 +2943,36 @@ EOT
2937
2943
  verify_args!(args:args, optparse:optparse, count:1)
2938
2944
  connect(options)
2939
2945
  instance = find_instance_by_name_or_id(args[0])
2940
- unless options[:yes] || ::Morpheus::Cli::OptionTypes::confirm("Are you sure you would like to snapshot the instance '#{instance['name']}'?", options)
2941
- exit 1
2942
- end
2946
+
2943
2947
  payload = {}
2944
2948
  if options[:payload]
2945
2949
  payload = options[:payload]
2946
2950
  payload.deep_merge!({'snapshot' => parse_passed_options(options)})
2947
2951
  else
2948
2952
  payload.deep_merge!({'snapshot' => parse_passed_options(options)})
2953
+ snapshot = payload['snapshot']
2954
+ # prompt for name and description
2955
+ name = prompt_value({'fieldName' => 'name', 'type' => 'text', 'fieldLabel' => 'Snapshot Name', 'description' => "Snapshot Name. Default is \"{name}.{timestamp}\""}, options)
2956
+ snapshot['name'] = name if !name.to_s.empty?
2957
+ description = prompt_value({'fieldName' => 'description', 'type' => 'text', 'fieldLabel' => 'Description', 'description' => "Snapshot Description."}, options)
2958
+ snapshot['description'] = description if !description.to_s.empty?
2959
+ # need to GET provision type for some settings...
2960
+ provision_type = nil
2961
+ begin
2962
+ provision_type = @provision_types_interface.get(instance['layout']['provisionTypeId'])['provisionType']
2963
+ rescue => ex
2964
+ Morpheus::Logging::DarkPrinter.puts "Failed to load provision type!" if Morpheus::Logging.debug?
2965
+ end
2966
+ if provision_type && provision_type['hasMemorySnapshots']
2967
+ # prompt for memorySnapshot
2968
+ memory_snapshot = prompt_value({'fieldName' => 'memorySnapshot', 'type' => 'checkbox', 'fieldLabel' => 'Memory Snapshot?', 'description' => "Snapshot Description."}, options)
2969
+ snapshot['memorySnapshot'] = memory_snapshot if !memory_snapshot.to_s.empty?
2970
+ end
2971
+ # convert "on" and "off" to true/false
2972
+ payload.booleanize!
2973
+ end
2974
+ unless options[:yes] || ::Morpheus::Cli::OptionTypes::confirm("Are you sure you would like to snapshot the instance '#{instance['name']}'?", options)
2975
+ exit 1
2949
2976
  end
2950
2977
  @instances_interface.setopts(options)
2951
2978
  if options[:dry_run]
@@ -3470,17 +3497,18 @@ EOT
3470
3497
  def snapshots(args)
3471
3498
  options = {}
3472
3499
  optparse = Morpheus::Cli::OptionParser.new do |opts|
3473
- opts.banner = subcommand_usage("[instance]")
3500
+ opts.banner = subcommand_usage("[instance] [snapshot]")
3474
3501
  # no pagination yet
3475
3502
  # build_standard_list_options(opts, options)
3476
- build_standard_get_options(opts, options)
3503
+ build_standard_list_options(opts, options, [:details])
3477
3504
  opts.footer = <<-EOT
3478
3505
  List snapshots for an instance.
3479
3506
  [instance] is required. This is the name or id of an instance
3507
+ [snapshot] is optional. this is the name or id a snapshot to filter by.
3480
3508
  EOT
3481
3509
  end
3482
3510
  optparse.parse!(args)
3483
- verify_args!(args:args, optparse:optparse, count:1)
3511
+ verify_args!(args:args, optparse:optparse, min:1, max: 2)
3484
3512
  connect(options)
3485
3513
  begin
3486
3514
  instance = find_instance_by_name_or_id(args[0])
@@ -3491,25 +3519,52 @@ EOT
3491
3519
  return
3492
3520
  end
3493
3521
  json_response = @instances_interface.snapshots(instance['id'], params)
3494
- snapshots = json_response['snapshots']
3522
+ snapshots = json_response['snapshots']
3523
+ # [snapshots] is done with post api filtering by id or name or externalId
3524
+ if args[1]
3525
+ if args[1] =~ /\A\d{1,}\Z/
3526
+ snapshots = snapshots.select {|it| it['id'].to_s == args[1] }
3527
+ else
3528
+ # snapshots = snapshots.select {|it| it['name'] == args[1] || it['externalId'] == args[1] }
3529
+ # match beginning of name of externalId
3530
+ snapshots = snapshots.select {|it| it['name'].to_s.index(args[1]) == 0 || it['externalId'].to_s.index(args[1]) == 0 }
3531
+ end
3532
+ json_response['snapshots'] = snapshots # update response for -j filtering too
3533
+ end
3495
3534
  render_response(json_response, options, 'snapshots') do
3496
3535
  print_h1 "Snapshots: #{instance['name']} (#{instance['instanceType']['name']})", [], options
3497
3536
  if snapshots.empty?
3498
- print cyan,"No snapshots found",reset,"\n"
3537
+ if args[1]
3538
+ print cyan,"No snapshots found for '#{args[1]}'",reset,"\n"
3539
+ elsif
3540
+ print cyan,"No snapshots found",reset,"\n"
3541
+ end
3542
+ print reset, "\n"
3499
3543
  else
3500
- snapshot_column_definitions = {
3501
- "ID" => lambda {|it| it['id'] },
3502
- "Name" => lambda {|it| it['name'] },
3503
- "Description" => lambda {|it| it['description'] },
3504
- # "Type" => lambda {|it| it['snapshotType'] },
3505
- "Date Created" => lambda {|it| format_local_dt(it['snapshotCreated']) },
3506
- "Status" => lambda {|it| format_snapshot_status(it) }
3507
- }
3508
- print cyan
3509
- print as_pretty_table(snapshots, snapshot_column_definitions.upcase_keys!, options)
3510
- print_results_pagination({size: snapshots.size, total: snapshots.size})
3544
+ if options[:details]
3545
+ if snapshots.size > 3
3546
+ print cyan, "Showing first 3 snapshots. Use the ID to get more details.", reset, "\n"
3547
+ snapshots = snapshots.first(3) # this actually makes a request for each one here so don't go crazy...
3548
+ end
3549
+ snapshots.each do |snapshot|
3550
+ Morpheus::Cli::Snapshots.new.handle(["get", snapshot['id']] + (options[:remote] ? ["-r",options[:remote]] : []))
3551
+ end
3552
+ else
3553
+ # Snapshots List
3554
+ snapshot_column_definitions = {
3555
+ "ID" => lambda {|it| it['id'] },
3556
+ "Name" => lambda {|it| it['name'] },
3557
+ "Description" => lambda {|it| it['description'] },
3558
+ # "Type" => lambda {|it| it['snapshotType'] },
3559
+ "Date Created" => lambda {|it| format_local_dt(it['snapshotCreated']) },
3560
+ "Status" => lambda {|it| format_snapshot_status(it) }
3561
+ }
3562
+ print cyan
3563
+ print as_pretty_table(snapshots, snapshot_column_definitions.upcase_keys!, options)
3564
+ print_results_pagination({size: snapshots.size, total: snapshots.size})
3565
+ print reset, "\n"
3566
+ end
3511
3567
  end
3512
- print reset, "\n"
3513
3568
  end
3514
3569
  return 0
3515
3570
  rescue RestClient::Exception => e
@@ -77,11 +77,15 @@ class Morpheus::Cli::Snapshots
77
77
  "Snapshot Type" => 'snapshotType',
78
78
  "Cloud" => lambda {|it| format_name_and_id(it['zone']) },
79
79
  "Datastore" => lambda {|it| format_name_and_id(it['datastore']) },
80
+ "Memory Snapshot" => lambda {|it| format_boolean(it['memorySnapshot']) },
81
+ "For Export" => lambda {|it| format_boolean(it['forExport']) },
80
82
  "Parent Snapshot" => lambda {|it| format_name_and_id(it['parentSnapshot']) },
81
83
  "Active" => lambda {|it| format_boolean(it['currentlyActive']) },
82
84
  "Date Created" => lambda {|it| format_local_dt(it['dateCreated']) },
83
85
  "Status" => lambda {|it| format_snapshot_status(it) }
84
86
  }
87
+ description_cols.delete("Memory Snapshot") if !snapshot['memorySnapshot']
88
+ description_cols.delete("For Export") if !snapshot['forExport']
85
89
  print_description_list(description_cols, snapshot)
86
90
 
87
91
  print reset, "\n"
@@ -1,6 +1,6 @@
1
1
 
2
2
  module Morpheus
3
3
  module Cli
4
- VERSION = "8.0.7"
4
+ VERSION = "8.0.8"
5
5
  end
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: morpheus-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.0.7
4
+ version: 8.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Estes
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2025-06-13 00:00:00.000000000 Z
14
+ date: 2025-07-23 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: public_suffix