morpheus-cli 2.10.0 → 2.10.1
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 +4 -4
- data/bin/morpheus +27 -32
- data/lib/morpheus/api/accounts_interface.rb +36 -47
- data/lib/morpheus/api/api_client.rb +141 -110
- data/lib/morpheus/api/app_templates_interface.rb +56 -72
- data/lib/morpheus/api/apps_interface.rb +111 -132
- data/lib/morpheus/api/auth_interface.rb +30 -0
- data/lib/morpheus/api/clouds_interface.rb +71 -76
- data/lib/morpheus/api/custom_instance_types_interface.rb +21 -46
- data/lib/morpheus/api/dashboard_interface.rb +10 -17
- data/lib/morpheus/api/deploy_interface.rb +60 -72
- data/lib/morpheus/api/deployments_interface.rb +53 -71
- data/lib/morpheus/api/groups_interface.rb +55 -45
- data/lib/morpheus/api/instance_types_interface.rb +19 -23
- data/lib/morpheus/api/instances_interface.rb +179 -177
- data/lib/morpheus/api/key_pairs_interface.rb +11 -17
- data/lib/morpheus/api/license_interface.rb +18 -23
- data/lib/morpheus/api/load_balancers_interface.rb +54 -69
- data/lib/morpheus/api/logs_interface.rb +25 -29
- data/lib/morpheus/api/options_interface.rb +13 -17
- data/lib/morpheus/api/provision_types_interface.rb +19 -22
- data/lib/morpheus/api/roles_interface.rb +75 -94
- data/lib/morpheus/api/security_group_rules_interface.rb +28 -37
- data/lib/morpheus/api/security_groups_interface.rb +39 -51
- data/lib/morpheus/api/servers_interface.rb +113 -115
- data/lib/morpheus/api/setup_interface.rb +31 -0
- data/lib/morpheus/api/task_sets_interface.rb +36 -38
- data/lib/morpheus/api/tasks_interface.rb +56 -69
- data/lib/morpheus/api/users_interface.rb +67 -76
- data/lib/morpheus/api/virtual_images_interface.rb +61 -61
- data/lib/morpheus/api/whoami_interface.rb +12 -15
- data/lib/morpheus/cli.rb +71 -60
- data/lib/morpheus/cli/accounts.rb +254 -315
- data/lib/morpheus/cli/alias_command.rb +219 -0
- data/lib/morpheus/cli/app_templates.rb +264 -272
- data/lib/morpheus/cli/apps.rb +608 -671
- data/lib/morpheus/cli/cli_command.rb +259 -21
- data/lib/morpheus/cli/cli_registry.rb +99 -14
- data/lib/morpheus/cli/clouds.rb +599 -372
- data/lib/morpheus/cli/config_file.rb +126 -0
- data/lib/morpheus/cli/credentials.rb +141 -117
- data/lib/morpheus/cli/dashboard_command.rb +48 -56
- data/lib/morpheus/cli/deployments.rb +254 -268
- data/lib/morpheus/cli/deploys.rb +150 -142
- data/lib/morpheus/cli/error_handler.rb +38 -0
- data/lib/morpheus/cli/groups.rb +551 -179
- data/lib/morpheus/cli/hosts.rb +862 -617
- data/lib/morpheus/cli/instance_types.rb +103 -95
- data/lib/morpheus/cli/instances.rb +1335 -1009
- data/lib/morpheus/cli/key_pairs.rb +82 -90
- data/lib/morpheus/cli/library.rb +498 -499
- data/lib/morpheus/cli/license.rb +83 -101
- data/lib/morpheus/cli/load_balancers.rb +314 -300
- data/lib/morpheus/cli/login.rb +66 -44
- data/lib/morpheus/cli/logout.rb +47 -46
- data/lib/morpheus/cli/mixins/accounts_helper.rb +69 -31
- data/lib/morpheus/cli/mixins/infrastructure_helper.rb +106 -0
- data/lib/morpheus/cli/mixins/print_helper.rb +181 -17
- data/lib/morpheus/cli/mixins/provisioning_helper.rb +535 -458
- data/lib/morpheus/cli/mixins/whoami_helper.rb +2 -2
- data/lib/morpheus/cli/option_parser.rb +35 -0
- data/lib/morpheus/cli/option_types.rb +232 -192
- data/lib/morpheus/cli/recent_activity_command.rb +61 -65
- data/lib/morpheus/cli/remote.rb +446 -199
- data/lib/morpheus/cli/roles.rb +884 -906
- data/lib/morpheus/cli/security_group_rules.rb +213 -203
- data/lib/morpheus/cli/security_groups.rb +237 -192
- data/lib/morpheus/cli/shell.rb +338 -231
- data/lib/morpheus/cli/tasks.rb +326 -308
- data/lib/morpheus/cli/users.rb +457 -462
- data/lib/morpheus/cli/version.rb +1 -1
- data/lib/morpheus/cli/version_command.rb +16 -18
- data/lib/morpheus/cli/virtual_images.rb +526 -345
- data/lib/morpheus/cli/whoami.rb +125 -111
- data/lib/morpheus/cli/workflows.rb +338 -185
- data/lib/morpheus/formatters.rb +8 -1
- data/lib/morpheus/logging.rb +1 -1
- data/lib/morpheus/rest_client.rb +17 -8
- metadata +9 -3
- data/lib/morpheus/api/custom_instance_types.rb +0 -55
data/lib/morpheus/cli/deploys.rb
CHANGED
@@ -9,146 +9,154 @@ require 'morpheus/cli/cli_command'
|
|
9
9
|
class Morpheus::Cli::Deploys
|
10
10
|
include Morpheus::Cli::CliCommand
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
12
|
+
set_command_name :deploy
|
13
|
+
|
14
|
+
def initialize()
|
15
|
+
# @appliance_name, @appliance_url = Morpheus::Cli::Remote.active_appliance
|
16
|
+
end
|
17
|
+
|
18
|
+
def connect(opts)
|
19
|
+
@api_client = establish_remote_appliance_connection(opts)
|
20
|
+
@instances_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).instances
|
21
|
+
@deploy_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).deploy
|
22
|
+
end
|
23
|
+
|
24
|
+
def handle(args)
|
25
|
+
deploy(args)
|
26
|
+
end
|
27
|
+
|
28
|
+
def deploy(args)
|
29
|
+
options={}
|
30
|
+
optparse = Morpheus::Cli::OptionParser.new do|opts|
|
31
|
+
opts.banner = "Usage: morpheus deploy [environment]"
|
32
|
+
build_common_options(opts, options, [])
|
33
|
+
opts.footer = "Deploy to an environment using the morpheus.yml file, located in the working directory."
|
34
|
+
# "todo: document me better!"
|
35
|
+
end
|
36
|
+
optparse.parse!(args)
|
37
|
+
connect(options)
|
38
|
+
environment = 'production' # yikes!
|
39
|
+
if args.count > 0
|
40
|
+
environment = args[0]
|
41
|
+
end
|
42
|
+
if load_deploy_file().nil?
|
43
|
+
puts "Morpheus Deploy File `morpheus.yml` not detected. Please create one and try again."
|
44
|
+
return
|
45
|
+
end
|
46
|
+
|
47
|
+
deploy_args = merged_deploy_args(environment)
|
48
|
+
if deploy_args['name'].nil?
|
49
|
+
puts "Instance not specified. Please specify the instance name and try again."
|
50
|
+
return
|
51
|
+
end
|
52
|
+
|
53
|
+
instance_results = @instances_interface.get(name: deploy_args['name'])
|
54
|
+
if instance_results['instances'].empty?
|
55
|
+
puts "Instance not found by name #{args[0]}"
|
56
|
+
return
|
57
|
+
end
|
58
|
+
instance = instance_results['instances'][0]
|
59
|
+
instance_id = instance['id']
|
60
|
+
print "\n" ,cyan, bold, "Morpheus Deployment\n","==================", reset, "\n\n"
|
61
|
+
|
62
|
+
if !deploy_args['script'].nil?
|
63
|
+
print cyan, bold, " - Executing Pre Deploy Script...", reset, "\n"
|
64
|
+
|
65
|
+
if !system(deploy_args['script'])
|
66
|
+
puts "Error executing pre script..."
|
67
|
+
return
|
68
|
+
end
|
69
|
+
end
|
70
|
+
# Create a new deployment record
|
71
|
+
deploy_result = @deploy_interface.create(instance_id)
|
72
|
+
app_deploy = deploy_result['appDeploy']
|
73
|
+
deployment_id = app_deploy['id']
|
74
|
+
|
75
|
+
# Upload Files
|
76
|
+
print "\n",cyan, bold, "Uploading Files...", reset, "\n"
|
77
|
+
current_working_dir = Dir.pwd
|
78
|
+
deploy_args['files'].each do |fmap|
|
79
|
+
Dir.chdir(fmap['path'] || current_working_dir)
|
80
|
+
files = Dir.glob(fmap['pattern'] || '**/*')
|
81
|
+
files.each do |file|
|
82
|
+
if File.file?(file)
|
83
|
+
print cyan,bold, " - Uploading #{file} ...", reset, "\n"
|
84
|
+
destination = file.split("/")[0..-2].join("/")
|
85
|
+
@deploy_interface.upload_file(deployment_id,file,destination)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
print cyan, bold, "Upload Complete!", reset, "\n"
|
90
|
+
Dir.chdir(current_working_dir)
|
91
|
+
|
92
|
+
if !deploy_args['post_script'].nil?
|
93
|
+
print cyan, bold, "Executing Post Script...", reset, "\n"
|
94
|
+
if !system(deploy_args['post_script'])
|
95
|
+
puts "Error executing post script..."
|
96
|
+
return
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
deploy_payload = {}
|
101
|
+
if deploy_args['env']
|
102
|
+
evars = []
|
103
|
+
deploy_args['env'].each_pair do |key, value|
|
104
|
+
evars << {name: key, value: value, export: false}
|
105
|
+
end
|
106
|
+
payload = {envs: evars}
|
107
|
+
@instances_interface.create_env(instance_id, payload)
|
108
|
+
@instances_interface.restart(instance_id)
|
109
|
+
end
|
110
|
+
if deploy_args['options']
|
111
|
+
deploy_payload = {
|
112
|
+
appDeploy: {
|
113
|
+
config: deploy_args['options']
|
114
|
+
}
|
115
|
+
}
|
116
|
+
end
|
117
|
+
|
118
|
+
print cyan, bold, "Deploying to Servers...", reset, "\n"
|
119
|
+
@deploy_interface.deploy(deployment_id,deploy_payload)
|
120
|
+
print cyan, bold, "Deploy Successful!", reset, "\n"
|
121
|
+
end
|
122
|
+
|
123
|
+
def list(args)
|
124
|
+
end
|
125
|
+
|
126
|
+
def rollback(args)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Loads a morpheus.yml file from within the current working directory.
|
130
|
+
# This file contains information necessary in the project to perform a deployment via the cli
|
131
|
+
#
|
132
|
+
# === Example File Attributes
|
133
|
+
# * +script+ - The initial script to run before uploading files
|
134
|
+
# * +name+ - The instance name we are deploying to (can be overridden in CLI)
|
135
|
+
# * +remote+ - Optional remote appliance name we are connecting to
|
136
|
+
# * +files+ - List of file patterns to use for uploading files and their target destination
|
137
|
+
# * +options+ - Map of deployment options depending on deployment type
|
138
|
+
# * +post_script+ - A post operation script to be run on the local machine
|
139
|
+
# * +stage_deploy+ - If set to true the deploy will only be staged and not actually run
|
140
|
+
#
|
141
|
+
# +NOTE: + It is also possible to nest these properties in an "environments" map to override based on a passed environment deploy name
|
142
|
+
#
|
143
|
+
def load_deploy_file
|
144
|
+
if !File.exist? "morpheus.yml"
|
145
|
+
puts "No morpheus.yml file detected in the current directory. Nothing to do."
|
146
|
+
return nil
|
147
|
+
end
|
148
|
+
|
149
|
+
@deploy_file = YAML.load_file("morpheus.yml")
|
150
|
+
return @deploy_file
|
151
|
+
end
|
152
|
+
|
153
|
+
def merged_deploy_args(environment)
|
154
|
+
environment = environment || production
|
155
|
+
|
156
|
+
deploy_args = @deploy_file.reject { |key,value| key == 'environment'}
|
157
|
+
if !@deploy_file['environment'].nil? && !@deploy_file['environment'][environment].nil?
|
158
|
+
deploy_args = deploy_args.merge(@deploy_file['environment'][environment])
|
159
|
+
end
|
160
|
+
return deploy_args
|
161
|
+
end
|
154
162
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'term/ansicolor'
|
2
|
+
require 'optparse'
|
3
|
+
#require 'rest_client'
|
4
|
+
require 'morpheus/logging'
|
5
|
+
class Morpheus::Cli::ErrorHandler
|
6
|
+
include Morpheus::Cli::PrintHelper
|
7
|
+
|
8
|
+
def handle_error(err, options={})
|
9
|
+
# heh
|
10
|
+
if Morpheus::Logging.debug? && options[:debug].nil?
|
11
|
+
options[:debug] = true
|
12
|
+
end
|
13
|
+
case (err)
|
14
|
+
when OptionParser::InvalidOption, OptionParser::AmbiguousOption, OptionParser::MissingArgument, OptionParser::InvalidArgument
|
15
|
+
# raise e
|
16
|
+
print_red_alert "#{err.message}"
|
17
|
+
puts "Try -h for help with this command."
|
18
|
+
when Errno::ECONNREFUSED
|
19
|
+
print_red_alert "#{err.message}"
|
20
|
+
# more special errors?
|
21
|
+
when RestClient::Exception
|
22
|
+
print_rest_exception(err, options)
|
23
|
+
else
|
24
|
+
print_red_alert "Unexpected Error."
|
25
|
+
if !options[:debug]
|
26
|
+
print "Use --debug for more information.\n"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
if options[:debug]
|
31
|
+
print Term::ANSIColor.red, "\n", "#{err.class}: #{err.message}", "\n", Term::ANSIColor.reset
|
32
|
+
print err.backtrace.join("\n"), "\n\n"
|
33
|
+
else
|
34
|
+
#print "Use --debug for more information.\n"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
data/lib/morpheus/cli/groups.rb
CHANGED
@@ -4,187 +4,559 @@ require 'rest_client'
|
|
4
4
|
require 'optparse'
|
5
5
|
require 'table_print'
|
6
6
|
require 'morpheus/cli/cli_command'
|
7
|
+
require 'morpheus/cli/mixins/infrastructure_helper'
|
8
|
+
require 'morpheus/logging'
|
7
9
|
|
8
10
|
class Morpheus::Cli::Groups
|
9
11
|
include Morpheus::Cli::CliCommand
|
12
|
+
include Morpheus::Cli::InfrastructureHelper
|
13
|
+
|
14
|
+
register_subcommands :list, :get, :add, :update, :use, :unuse, :add_cloud, :remove_cloud, :remove, :current => :print_current
|
15
|
+
alias_subcommand :details, :get
|
16
|
+
set_default_subcommand :list
|
17
|
+
|
18
|
+
def initialize()
|
19
|
+
# @appliance_name, @appliance_url = Morpheus::Cli::Remote.active_appliance
|
20
|
+
end
|
21
|
+
|
22
|
+
def connect(opts)
|
23
|
+
@api_client = establish_remote_appliance_connection(opts)
|
24
|
+
@groups_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).groups
|
25
|
+
@clouds_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).clouds
|
26
|
+
@active_group_id = Morpheus::Cli::Groups.active_groups[@appliance_name]
|
27
|
+
end
|
28
|
+
|
29
|
+
def handle(args)
|
30
|
+
handle_subcommand(args)
|
31
|
+
end
|
32
|
+
|
33
|
+
def list(args)
|
34
|
+
options = {}
|
35
|
+
params = {}
|
36
|
+
optparse = Morpheus::Cli::OptionParser.new do|opts|
|
37
|
+
opts.banner = subcommand_usage()
|
38
|
+
build_common_options(opts, options, [:list, :json, :remote])
|
39
|
+
opts.footer = "This outputs a paginated list of groups."
|
40
|
+
end
|
41
|
+
optparse.parse!(args)
|
42
|
+
connect(options)
|
43
|
+
begin
|
44
|
+
[:phrase, :offset, :max, :sort, :direction].each do |k|
|
45
|
+
params[k] = options[k] unless options[k].nil?
|
46
|
+
end
|
47
|
+
json_response = @groups_interface.get(params)
|
48
|
+
if options[:json]
|
49
|
+
print JSON.pretty_generate(json_response)
|
50
|
+
print "\n"
|
51
|
+
return
|
52
|
+
end
|
53
|
+
groups = json_response['groups']
|
54
|
+
print "\n" ,cyan, bold, "Morpheus Groups\n","==================", reset, "\n\n"
|
55
|
+
if groups.empty?
|
56
|
+
puts yellow,"No groups currently configured.",reset
|
57
|
+
else
|
58
|
+
print_groups_table(groups)
|
59
|
+
print_results_pagination(json_response)
|
60
|
+
if @active_group_id
|
61
|
+
active_group = groups.find { |it| it['id'] == @active_group_id }
|
62
|
+
active_group = active_group || find_group_by_name_or_id(@active_group_id)
|
63
|
+
#unless @appliances.keys.size == 1
|
64
|
+
print cyan, "\n# => Currently using #{active_group['name']}\n", reset
|
65
|
+
#end
|
66
|
+
else
|
67
|
+
unless options[:remote]
|
68
|
+
print "\n# => No active group, see `groups use`\n", reset
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
print reset,"\n"
|
73
|
+
rescue RestClient::Exception => e
|
74
|
+
print_rest_exception(e, options)
|
75
|
+
exit 1
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def get(args)
|
80
|
+
options = {}
|
81
|
+
optparse = Morpheus::Cli::OptionParser.new do|opts|
|
82
|
+
opts.banner = subcommand_usage("[name]")
|
83
|
+
build_common_options(opts, options, [:json, :remote])
|
84
|
+
opts.footer = "This outputs details about a specific group."
|
85
|
+
end
|
86
|
+
optparse.parse!(args)
|
87
|
+
if args.count < 1
|
88
|
+
puts optparse
|
89
|
+
exit 1
|
90
|
+
end
|
91
|
+
connect(options)
|
92
|
+
begin
|
93
|
+
group = find_group_by_name_or_id(args[0])
|
94
|
+
#json_response = @groups_interface.get(group['id'])
|
95
|
+
json_response = {'group' => group}
|
96
|
+
if options[:json]
|
97
|
+
print JSON.pretty_generate(json_response), "\n"
|
98
|
+
return
|
99
|
+
end
|
100
|
+
group = json_response['group']
|
101
|
+
|
102
|
+
is_active = @active_group_id && (@active_group_id == group['id'])
|
103
|
+
|
104
|
+
print "\n" ,cyan, bold, "Group Details\n","==================", reset, "\n\n"
|
105
|
+
print cyan
|
106
|
+
puts "ID: #{group['id']}"
|
107
|
+
puts "Name: #{group['name']}"
|
108
|
+
puts "Code: #{group['code']}"
|
109
|
+
puts "Location: #{group['location']}"
|
110
|
+
puts "Clouds: #{group['zones'].collect {|it| it['name'] }.join(', ')}"
|
111
|
+
puts "Hosts: #{group['serverCount']}"
|
112
|
+
if is_active
|
113
|
+
puts "\n => This is the active group."
|
114
|
+
end
|
115
|
+
|
116
|
+
print reset,"\n"
|
117
|
+
|
118
|
+
#puts instance
|
119
|
+
rescue RestClient::Exception => e
|
120
|
+
print_rest_exception(e, options)
|
121
|
+
exit 1
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def add(args)
|
126
|
+
options = {}
|
127
|
+
params = {}
|
128
|
+
use_it = false
|
129
|
+
optparse = Morpheus::Cli::OptionParser.new do|opts|
|
130
|
+
opts.banner = subcommand_usage("[name]")
|
131
|
+
opts.on( '-l', '--location LOCATION', "Location" ) do |val|
|
132
|
+
params[:location] = val
|
133
|
+
end
|
134
|
+
opts.on( '--use', '--use', "Make this the current active group" ) do
|
135
|
+
use_it = true
|
136
|
+
end
|
137
|
+
build_common_options(opts, options, [:options, :json, :dry_run, :remote])
|
138
|
+
opts.footer = "Create a new group."
|
139
|
+
end
|
140
|
+
optparse.parse!(args)
|
141
|
+
# if args.count < 1
|
142
|
+
# puts optparse
|
143
|
+
# exit 1
|
144
|
+
# end
|
145
|
+
connect(options)
|
146
|
+
begin
|
147
|
+
# group = {name: args[0], location: params[:location]}
|
148
|
+
# payload = {group: group}
|
149
|
+
group_payload = {}
|
150
|
+
if args[0]
|
151
|
+
group_payload[:name] = args[0]
|
152
|
+
options[:options]['name'] = args[0] # to skip prompt
|
153
|
+
end
|
154
|
+
if params[:location]
|
155
|
+
group_payload[:name] = params[:location]
|
156
|
+
options[:options]['location'] = params[:location] # to skip prompt
|
157
|
+
end
|
158
|
+
all_option_types = add_group_option_types()
|
159
|
+
params = Morpheus::Cli::OptionTypes.prompt(all_option_types, options[:options], @api_client, {})
|
160
|
+
group_payload.merge!(params)
|
161
|
+
payload = {group: group_payload}
|
162
|
+
|
163
|
+
if options[:dry_run]
|
164
|
+
print_dry_run @groups_interface.dry.create(payload)
|
165
|
+
return
|
166
|
+
end
|
167
|
+
json_response = @groups_interface.create(payload)
|
168
|
+
group = json_response['group']
|
169
|
+
if use_it
|
170
|
+
::Morpheus::Cli::Groups.set_active_group(@appliance_name, group['id'])
|
171
|
+
end
|
172
|
+
if options[:json]
|
173
|
+
print JSON.pretty_generate(json_response)
|
174
|
+
print "\n"
|
175
|
+
else
|
176
|
+
print_green_success "Added group #{group['name']}"
|
177
|
+
list([])
|
178
|
+
end
|
179
|
+
rescue RestClient::Exception => e
|
180
|
+
print_rest_exception(e, options)
|
181
|
+
exit 1
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def update(args)
|
186
|
+
options = {}
|
187
|
+
params = {}
|
188
|
+
optparse = Morpheus::Cli::OptionParser.new do|opts|
|
189
|
+
opts.banner = subcommand_usage("[name] [options]")
|
190
|
+
opts.on( '-l', '--location LOCATION', "Location" ) do |val|
|
191
|
+
params[:location] = val
|
192
|
+
end
|
193
|
+
build_common_options(opts, options, [:options, :json, :dry_run, :remote])
|
194
|
+
opts.footer = "Update an existing group."
|
195
|
+
end
|
196
|
+
optparse.parse!(args)
|
197
|
+
if args.count < 1
|
198
|
+
puts optparse
|
199
|
+
exit 1
|
200
|
+
end
|
201
|
+
connect(options)
|
202
|
+
begin
|
203
|
+
group = find_group_by_name_or_id(args[0])
|
204
|
+
group_payload = {id: group['id']}
|
205
|
+
|
206
|
+
all_option_types = update_group_option_types()
|
207
|
+
#params = Morpheus::Cli::OptionTypes.prompt(all_option_types, options[:options], @api_client, {})
|
208
|
+
params = options[:options] || {}
|
209
|
+
|
210
|
+
if params.empty?
|
211
|
+
puts optparse.banner
|
212
|
+
print_available_options(all_option_types)
|
213
|
+
exit 1
|
214
|
+
end
|
215
|
+
|
216
|
+
group_payload.merge!(params)
|
217
|
+
|
218
|
+
payload = {group: group_payload}
|
219
|
+
|
220
|
+
if options[:dry_run]
|
221
|
+
print_dry_run @groups_interface.dry.update(group['id'], payload)
|
222
|
+
return
|
223
|
+
end
|
224
|
+
json_response = @groups_interface.update(group['id'], payload)
|
225
|
+
if options[:json]
|
226
|
+
print JSON.pretty_generate(json_response)
|
227
|
+
print "\n"
|
228
|
+
else
|
229
|
+
#list([])
|
230
|
+
get([group["id"]])
|
231
|
+
end
|
232
|
+
rescue RestClient::Exception => e
|
233
|
+
print_rest_exception(e, options)
|
234
|
+
exit 1
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def add_cloud(args)
|
239
|
+
options = {}
|
240
|
+
optparse = Morpheus::Cli::OptionParser.new do|opts|
|
241
|
+
opts.banner = subcommand_usage("[name]", "CLOUD")
|
242
|
+
build_common_options(opts, options, [:json, :dry_run, :remote])
|
243
|
+
opts.footer = "Add a cloud to a group."
|
244
|
+
end
|
245
|
+
optparse.parse!(args)
|
246
|
+
if args.count < 2
|
247
|
+
puts optparse
|
248
|
+
exit 1
|
249
|
+
end
|
250
|
+
connect(options)
|
251
|
+
begin
|
252
|
+
group = find_group_by_name_or_id(args[0])
|
253
|
+
cloud = find_cloud_by_name_or_id(args[1])
|
254
|
+
current_zones = group['zones']
|
255
|
+
found_zone = current_zones.find {|it| it["id"] == cloud["id"] }
|
256
|
+
if found_zone
|
257
|
+
print_red_alert "Cloud #{cloud['name']} is already in group #{group['name']}."
|
258
|
+
exit 1
|
259
|
+
end
|
260
|
+
new_zones = current_zones + [{'id' => cloud['id']}]
|
261
|
+
payload = {group: {id: group["id"], zones: new_zones}}
|
262
|
+
if options[:dry_run]
|
263
|
+
print_dry_run @groups_interface.dry.update_zones(group["id"], payload)
|
264
|
+
return
|
265
|
+
end
|
266
|
+
json_response = @groups_interface.update_zones(group["id"], payload)
|
267
|
+
if options[:json]
|
268
|
+
print JSON.pretty_generate(json_response)
|
269
|
+
print "\n"
|
270
|
+
else
|
271
|
+
print_green_success "Added cloud #{cloud["id"]} to group #{group['name']}"
|
272
|
+
#list([])
|
273
|
+
get([group["id"]])
|
274
|
+
end
|
275
|
+
rescue RestClient::Exception => e
|
276
|
+
print_rest_exception(e, options)
|
277
|
+
exit 1
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def remove_cloud(args)
|
282
|
+
options = {}
|
283
|
+
optparse = Morpheus::Cli::OptionParser.new do|opts|
|
284
|
+
opts.banner = subcommand_usage("[name]", "CLOUD")
|
285
|
+
build_common_options(opts, options, [:json, :dry_run, :remote])
|
286
|
+
opts.footer = "Remove a cloud from a group."
|
287
|
+
end
|
288
|
+
optparse.parse!(args)
|
289
|
+
if args.count < 2
|
290
|
+
puts optparse
|
291
|
+
exit 1
|
292
|
+
end
|
293
|
+
connect(options)
|
294
|
+
begin
|
295
|
+
group = find_group_by_name_or_id(args[0])
|
296
|
+
cloud = find_cloud_by_name_or_id(args[1])
|
297
|
+
current_zones = group['zones']
|
298
|
+
found_zone = current_zones.find {|it| it["id"] == cloud["id"] }
|
299
|
+
if !found_zone
|
300
|
+
print_red_alert "Cloud #{cloud['name']} is not in group #{group['name']}."
|
301
|
+
exit 1
|
302
|
+
end
|
303
|
+
new_zones = current_zones.reject {|it| it["id"] == cloud["id"] }
|
304
|
+
payload = {group: {id: group["id"], zones: new_zones}}
|
305
|
+
if options[:dry_run]
|
306
|
+
print_dry_run @groups_interface.dry.update_zones(group["id"], payload)
|
307
|
+
return
|
308
|
+
end
|
309
|
+
json_response = @groups_interface.update_zones(group["id"], payload)
|
310
|
+
if options[:json]
|
311
|
+
print JSON.pretty_generate(json_response)
|
312
|
+
print "\n"
|
313
|
+
else
|
314
|
+
print_green_success "Removed cloud #{cloud['name']} from group #{group['name']}"
|
315
|
+
# list([])
|
316
|
+
get([group["id"]])
|
317
|
+
end
|
318
|
+
rescue RestClient::Exception => e
|
319
|
+
print_rest_exception(e, options)
|
320
|
+
exit 1
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
def remove(args)
|
325
|
+
options = {}
|
326
|
+
optparse = Morpheus::Cli::OptionParser.new do|opts|
|
327
|
+
opts.banner = subcommand_usage("[name]")
|
328
|
+
build_common_options(opts, options, [:json, :dry_run, :auto_confirm, :remote])
|
329
|
+
opts.footer = "Delete a group."
|
330
|
+
# more info to display here
|
331
|
+
end
|
332
|
+
optparse.parse!(args)
|
333
|
+
if args.count < 1
|
334
|
+
puts optparse
|
335
|
+
exit 1
|
336
|
+
end
|
337
|
+
connect(options)
|
338
|
+
|
339
|
+
begin
|
340
|
+
group = find_group_by_name_or_id(args[0])
|
341
|
+
unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the group #{group['name']}?")
|
342
|
+
exit
|
343
|
+
end
|
344
|
+
if options[:dry_run]
|
345
|
+
print_dry_run @groups_interface.dry.destroy(group['id'])
|
346
|
+
return
|
347
|
+
end
|
348
|
+
json_response = @groups_interface.destroy(group['id'])
|
349
|
+
if options[:json]
|
350
|
+
print JSON.pretty_generate(json_response)
|
351
|
+
print "\n"
|
352
|
+
elsif !options[:quiet]
|
353
|
+
print_green_success "Removed group #{group['name']}"
|
354
|
+
#list([])
|
355
|
+
end
|
356
|
+
rescue RestClient::Exception => e
|
357
|
+
print_rest_exception(e, options)
|
358
|
+
exit 1
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
def use(args)
|
363
|
+
options = {}
|
364
|
+
optparse = Morpheus::Cli::OptionParser.new do|opts|
|
365
|
+
opts.banner = subcommand_usage("[name]")
|
366
|
+
opts.footer = "" +
|
367
|
+
"This sets the current active group.\n" +
|
368
|
+
"It will be auto-selected for use during provisioning.\n" +
|
369
|
+
"You can still use the --group option to override this."
|
370
|
+
build_common_options(opts, options, [])
|
371
|
+
end
|
372
|
+
optparse.parse!(args)
|
373
|
+
if args.count < 1
|
374
|
+
puts optparse
|
375
|
+
exit 1
|
376
|
+
end
|
377
|
+
connect(options)
|
378
|
+
begin
|
379
|
+
# todo: this is a problem for unprivileged users, need to use find_group_by_id_for_provisioning(group_id)
|
380
|
+
group = find_group_by_name_or_id(args[0])
|
381
|
+
if !group
|
382
|
+
print_red_alert "Group not found by name #{args[0]}"
|
383
|
+
exit 1
|
384
|
+
end
|
385
|
+
|
386
|
+
if @active_group_id == group['id']
|
387
|
+
print reset,"Already using the group #{group['name']}","\n",reset
|
388
|
+
else
|
389
|
+
::Morpheus::Cli::Groups.set_active_group(@appliance_name, group['id'])
|
390
|
+
# ::Morpheus::Cli::Groups.save_groups(@active_groups)
|
391
|
+
#print cyan,"Switched active group to #{group['name']}","\n",reset
|
392
|
+
#list([])
|
393
|
+
end
|
394
|
+
rescue RestClient::Exception => e
|
395
|
+
print_rest_exception(e, options)
|
396
|
+
exit 1
|
397
|
+
end
|
398
|
+
|
399
|
+
end
|
400
|
+
|
401
|
+
def unuse(args)
|
402
|
+
options = {}
|
403
|
+
optparse = Morpheus::Cli::OptionParser.new do|opts|
|
404
|
+
opts.banner = subcommand_usage() + "\n\n" +
|
405
|
+
"This will clear the active group." + "\n" +
|
406
|
+
"You will be prompted for a Group during provisioning." + "\n\n"
|
407
|
+
build_common_options(opts, options, [])
|
408
|
+
end
|
409
|
+
optparse.parse!(args)
|
410
|
+
connect(options)
|
411
|
+
|
412
|
+
if @active_group_id
|
413
|
+
::Morpheus::Cli::Groups.clear_active_group(@appliance_name)
|
414
|
+
# unless options[:quiet]
|
415
|
+
# print cyan
|
416
|
+
# puts "Switched to no active group."
|
417
|
+
# puts "You will be prompted for Group during provisioning."
|
418
|
+
# print reset
|
419
|
+
# end
|
420
|
+
return true
|
421
|
+
else
|
422
|
+
puts "You are not using any group for appliance #{@appliance_name}"
|
423
|
+
#return false
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
def print_current(args)
|
428
|
+
options = {}
|
429
|
+
optparse = Morpheus::Cli::OptionParser.new do|opts|
|
430
|
+
opts.banner = subcommand_usage()
|
431
|
+
build_common_options(opts, options, [])
|
432
|
+
opts.footer = "" +
|
433
|
+
"This will set the current remote appliance.\n" +
|
434
|
+
"It will be used for all subsequent commands.\n" +
|
435
|
+
"You may still use the --remote option to override this."
|
436
|
+
end
|
437
|
+
optparse.parse!(args)
|
438
|
+
connect(options)
|
439
|
+
|
440
|
+
group = @active_group_id ? find_group_by_name_or_id(@active_group_id) : nil
|
441
|
+
if group
|
442
|
+
print cyan,group['name'].to_s,"\n",reset
|
443
|
+
else
|
444
|
+
print dark,"No active group. See `groups use`","\n",reset
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
protected
|
449
|
+
|
450
|
+
def print_groups_table(groups, opts={})
|
451
|
+
table_color = opts[:color] || cyan
|
452
|
+
active_group_id = @active_group_id # Morpheus::Cli::Groups.active_group
|
453
|
+
rows = groups.collect do |group|
|
454
|
+
is_active = @active_group_id && (@active_group_id == group['id'])
|
455
|
+
{
|
456
|
+
active: (is_active ? "=>" : ""),
|
457
|
+
id: group['id'],
|
458
|
+
name: group['name'],
|
459
|
+
location: group['location'],
|
460
|
+
cloud_count: group['zones'] ? group['zones'].size : 0,
|
461
|
+
server_count: group['serverCount']
|
462
|
+
}
|
463
|
+
end
|
464
|
+
columns = [
|
465
|
+
{:active => {:display_name => ""}},
|
466
|
+
{:id => {:width => 10}},
|
467
|
+
{:name => {:width => 16}},
|
468
|
+
{:location => {:width => 32}},
|
469
|
+
{:cloud_count => {:display_name => "Clouds"}},
|
470
|
+
{:server_count => {:display_name => "Hosts"}}
|
471
|
+
]
|
472
|
+
print table_color
|
473
|
+
tp rows, columns
|
474
|
+
print reset
|
475
|
+
end
|
476
|
+
|
477
|
+
def add_group_option_types()
|
478
|
+
tmp_option_types = [
|
479
|
+
{'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true, 'displayOrder' => 1},
|
480
|
+
{'fieldName' => 'code', 'fieldLabel' => 'Code', 'type' => 'text', 'required' => false, 'displayOrder' => 2},
|
481
|
+
{'fieldName' => 'location', 'fieldLabel' => 'Location', 'type' => 'text', 'required' => false, 'displayOrder' => 3}
|
482
|
+
]
|
483
|
+
|
484
|
+
# Advanced Options
|
485
|
+
# TODO: Service Registry
|
486
|
+
|
487
|
+
return tmp_option_types
|
488
|
+
end
|
489
|
+
|
490
|
+
def update_group_option_types()
|
491
|
+
add_group_option_types().collect {|it| it['required'] = false; it }
|
492
|
+
end
|
493
|
+
|
494
|
+
# todo: This belongs elsewhere, like module Morpheus::Cli::ActiveGroups
|
495
|
+
|
496
|
+
public
|
10
497
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
if args.count < 1
|
76
|
-
puts "\n#{optparse.banner}\n\n"
|
77
|
-
exit 1
|
78
|
-
end
|
79
|
-
|
80
|
-
begin
|
81
|
-
group_results = @groups_interface.get(args[0])
|
82
|
-
if group_results['groups'].empty?
|
83
|
-
print_red_alert "Group not found by name #{args[0]}"
|
84
|
-
exit 1
|
85
|
-
end
|
86
|
-
unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the group #{group_results['groups'][0]['name']}?")
|
87
|
-
exit
|
88
|
-
end
|
89
|
-
@groups_interface.destroy(group_results['groups'][0]['id'])
|
90
|
-
list([])
|
91
|
-
rescue RestClient::Exception => e
|
92
|
-
print_rest_exception(e, options)
|
93
|
-
exit 1
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
def list(args)
|
98
|
-
options = {}
|
99
|
-
optparse = OptionParser.new do|opts|
|
100
|
-
opts.banner = "Usage: morpheus groups list"
|
101
|
-
build_common_options(opts, options, [:json])
|
102
|
-
end
|
103
|
-
optparse.parse(args)
|
104
|
-
begin
|
105
|
-
json_response = @groups_interface.get()
|
106
|
-
if options[:json]
|
107
|
-
print JSON.pretty_generate(json_response)
|
108
|
-
return
|
109
|
-
end
|
110
|
-
groups = json_response['groups']
|
111
|
-
print "\n" ,cyan, bold, "Morpheus Groups\n","==================", reset, "\n\n"
|
112
|
-
if groups.empty?
|
113
|
-
puts yellow,"No groups currently configured.",reset
|
114
|
-
else
|
115
|
-
groups.each do |group|
|
116
|
-
if @active_groups[@appliance_name.to_sym] == group['id']
|
117
|
-
print cyan, bold, "=> #{group['name']} - #{group['location']}",reset,"\n"
|
118
|
-
else
|
119
|
-
print cyan, "= #{group['name']} - #{group['location']}\n",reset
|
120
|
-
end
|
121
|
-
end
|
122
|
-
end
|
123
|
-
print reset,"\n\n"
|
124
|
-
|
125
|
-
rescue RestClient::Exception => e
|
126
|
-
print_rest_exception(e, options)
|
127
|
-
exit 1
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
def use(args)
|
132
|
-
options = {}
|
133
|
-
optparse = OptionParser.new do|opts|
|
134
|
-
opts.banner = "Usage: morpheus groups use [name]"
|
135
|
-
build_common_options(opts, options, [])
|
136
|
-
end
|
137
|
-
optparse.parse(args)
|
138
|
-
if args.length < 1
|
139
|
-
puts "\n#{optparse.banner}\n\n"
|
140
|
-
exit 1
|
141
|
-
end
|
142
|
-
begin
|
143
|
-
json_response = @groups_interface.get(args[0])
|
144
|
-
groups = json_response['groups']
|
145
|
-
if groups.length > 0
|
146
|
-
@active_groups[@appliance_name.to_sym] = groups[0]['id']
|
147
|
-
::Morpheus::Cli::Groups.save_groups(@active_groups)
|
148
|
-
list([])
|
149
|
-
else
|
150
|
-
print_red_alert "Group not found by name #{args[0]}"
|
151
|
-
end
|
152
|
-
rescue RestClient::Exception => e
|
153
|
-
print_rest_exception(e, options)
|
154
|
-
exit 1
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
# Provides the current active group information
|
159
|
-
def self.active_group
|
160
|
-
appliance_name, appliance_url = Morpheus::Cli::Remote.active_appliance
|
161
|
-
if !defined?(@@groups)
|
162
|
-
@@groups = load_group_file
|
163
|
-
end
|
164
|
-
return @@groups[appliance_name.to_sym]
|
165
|
-
end
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
def self.load_group_file
|
170
|
-
remote_file = groups_file_path
|
171
|
-
if File.exist? remote_file
|
172
|
-
return YAML.load_file(remote_file)
|
173
|
-
else
|
174
|
-
{}
|
175
|
-
end
|
176
|
-
end
|
177
|
-
|
178
|
-
def self.groups_file_path
|
179
|
-
home_dir = Dir.home
|
180
|
-
morpheus_dir = File.join(home_dir,".morpheus")
|
181
|
-
if !Dir.exist?(morpheus_dir)
|
182
|
-
Dir.mkdir(morpheus_dir)
|
183
|
-
end
|
184
|
-
return File.join(morpheus_dir,"groups")
|
185
|
-
end
|
186
|
-
|
187
|
-
def self.save_groups(group_map)
|
188
|
-
File.open(groups_file_path, 'w') {|f| f.write group_map.to_yaml } #Store
|
189
|
-
end
|
498
|
+
@@groups = nil
|
499
|
+
|
500
|
+
class << self
|
501
|
+
include Term::ANSIColor
|
502
|
+
# Provides the current active group information
|
503
|
+
def active_groups_map
|
504
|
+
@@groups ||= load_group_file || {}
|
505
|
+
end
|
506
|
+
|
507
|
+
def active_groups
|
508
|
+
active_groups_map
|
509
|
+
end
|
510
|
+
|
511
|
+
# Provides the current active group information (just the ID right now)
|
512
|
+
# appliance_name should probably be required.. or just use this instead: Groups.active_groups[appliance_name]
|
513
|
+
def active_group(appliance_name=nil)
|
514
|
+
if appliance_name == nil
|
515
|
+
appliance_name, appliance_url = Morpheus::Cli::Remote.active_appliance
|
516
|
+
end
|
517
|
+
if !appliance_name
|
518
|
+
return nil
|
519
|
+
end
|
520
|
+
return active_groups_map[appliance_name.to_sym]
|
521
|
+
end
|
522
|
+
|
523
|
+
# alias (unused)
|
524
|
+
def active_group_id(appliance_name=nil)
|
525
|
+
active_group(appliance_name)
|
526
|
+
end
|
527
|
+
|
528
|
+
def set_active_group(appliance_name, group_id)
|
529
|
+
the_groups = active_groups_map
|
530
|
+
the_groups[appliance_name.to_sym] = group_id
|
531
|
+
save_groups(the_groups)
|
532
|
+
end
|
533
|
+
|
534
|
+
def clear_active_group(appliance_name)
|
535
|
+
the_groups = active_groups_map
|
536
|
+
the_groups.delete(appliance_name.to_sym)
|
537
|
+
save_groups(the_groups)
|
538
|
+
end
|
539
|
+
|
540
|
+
def load_group_file
|
541
|
+
fn = groups_file_path
|
542
|
+
if File.exist? fn
|
543
|
+
print "#{dark} #=> loading groups file #{fn}#{reset}\n" if Morpheus::Logging.debug?
|
544
|
+
return YAML.load_file(fn)
|
545
|
+
else
|
546
|
+
{}
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
def groups_file_path
|
551
|
+
return File.join(Morpheus::Cli.home_directory, "groups")
|
552
|
+
end
|
553
|
+
|
554
|
+
def save_groups(groups_map)
|
555
|
+
File.open(groups_file_path, 'w') {|f| f.write groups_map.to_yaml } #Store
|
556
|
+
FileUtils.chmod(0600, groups_file_path)
|
557
|
+
@@groups = groups_map
|
558
|
+
end
|
559
|
+
|
560
|
+
end
|
561
|
+
|
190
562
|
end
|