morpheus-cli 0.0.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.
@@ -0,0 +1,261 @@
1
+ # require 'yaml'
2
+ require 'io/console'
3
+ require 'rest_client'
4
+ require 'term/ansicolor'
5
+ require 'optparse'
6
+
7
+
8
+ class Morpheus::Cli::Servers
9
+ include Term::ANSIColor
10
+ def initialize()
11
+ @appliance_name, @appliance_url = Morpheus::Cli::Remote.active_appliance
12
+ @access_token = Morpheus::Cli::Credentials.new(@appliance_name,@appliance_url).request_credentials()
13
+ @servers_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).servers
14
+ @groups_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).groups
15
+ @zones_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).zones
16
+ @zone_types = @zones_interface.zone_types['zoneTypes']
17
+ @active_groups = ::Morpheus::Cli::Groups.load_group_file
18
+ end
19
+
20
+ def handle(args)
21
+ if @access_token.empty?
22
+ print red,bold, "\nInvalid Credentials. Unable to acquire access token. Please verify your credentials and try again.\n\n",reset
23
+ return 1
24
+ end
25
+ if args.empty?
26
+ puts "\nUsage: morpheus servers [list,add,remove] [name]\n\n"
27
+ end
28
+
29
+ case args[0]
30
+ when 'list'
31
+ list(args[1..-1])
32
+ when 'add'
33
+ add(args[1..-1])
34
+ when 'remove'
35
+ remove(args[1..-1])
36
+ else
37
+ puts "\nUsage: morpheus servers [list,add,remove] [name]\n\n"
38
+ end
39
+ end
40
+
41
+ def add(args)
42
+ if args.count < 2
43
+ puts "\nUsage: morpheus servers add ZONE [name]\n\n"
44
+ return
45
+ end
46
+ options = {zone: args[0]}
47
+
48
+ zone=nil
49
+ if !options[:group].nil?
50
+ group = find_group_by_name(options[:group])
51
+ if !group.nil?
52
+ options['groupId'] = group['id']
53
+ end
54
+ else
55
+ options['groupId'] = @active_groups[@appliance_name.to_sym]
56
+ end
57
+
58
+ if !options['groupId'].nil?
59
+ if !options[:zone].nil?
60
+ zone = find_zone_by_name(options['groupId'], options[:zone])
61
+ if !zone.nil?
62
+ options['zoneId'] = zone['id']
63
+ end
64
+ end
65
+ end
66
+
67
+ if options['zoneId'].nil?
68
+ puts red,bold,"\nEither the zone was not specified or was not found. Please make sure a zone is specified with --zone\n\n", reset
69
+ return
70
+ end
71
+
72
+ zone_type = zone_type_for_id(zone['zoneTypeId'])
73
+ begin
74
+ case zone_type['code']
75
+ when 'standard'
76
+ add_standard(args[1],options[:description],zone, args[2..-1])
77
+ list([])
78
+ when 'openstack'
79
+ add_openstack(args[1],options[:description],zone, args[2..-1])
80
+ list([])
81
+ when 'amazon'
82
+ add_amazon(args[1],options[:description],zone, args[2..-1])
83
+ list([])
84
+ else
85
+ puts "Unsupported Zone Type: This version of the morpheus cli does not support the requested zone type"
86
+ end
87
+ rescue RestClient::Exception => e
88
+ if e.response.code == 400
89
+ error = JSON.parse(e.response.to_s)
90
+ ::Morpheus::Cli::ErrorHandler.new.print_errors(error)
91
+ else
92
+ puts "Error Communicating with the Appliance. Please try again later. #{e}"
93
+ end
94
+ return nil
95
+ end
96
+ end
97
+
98
+ def remove(args)
99
+ if args.count < 1
100
+ puts "\nUsage: morpheus servers remove [name]\n\n"
101
+ return
102
+ end
103
+ begin
104
+ server_results = @servers_interface.get({name: args[0]})
105
+ if server_results['servers'].empty?
106
+ puts "Server not found by name #{args[0]}"
107
+ return
108
+ end
109
+ @servers_interface.destroy(server_results['servers'][0]['id'])
110
+ list([])
111
+ rescue RestClient::Exception => e
112
+ if e.response.code == 400
113
+ error = JSON.parse(e.response.to_s)
114
+ ::Morpheus::Cli::ErrorHandler.new.print_errors(error)
115
+ else
116
+ puts "Error Communicating with the Appliance. Please try again later. #{e}"
117
+ end
118
+ return nil
119
+ end
120
+ end
121
+
122
+ def list(args)
123
+ options = {}
124
+ optparse = OptionParser.new do|opts|
125
+ opts.on( '-g', '--group GROUP', "Group Name" ) do |group|
126
+ options[:group] = group
127
+ end
128
+ end
129
+ optparse.parse(args)
130
+ begin
131
+ params = {}
132
+ if !options[:group].nil?
133
+ group = find_group_by_name(options[:group])
134
+ if !group.nil?
135
+ params['site'] = group['id']
136
+ end
137
+ end
138
+
139
+ json_response = @servers_interface.get(params)
140
+ servers = json_response['servers']
141
+ print "\n" ,red, bold, "Morpheus Servers\n","==================", reset, "\n\n"
142
+ if servers.empty?
143
+ puts yellow,"No servers currently configured.",reset
144
+ else
145
+ servers.each do |server|
146
+ print red, "= #{server['name']} - #{server['description']} (#{server['status']})\n"
147
+ end
148
+ end
149
+ print reset,"\n\n"
150
+
151
+ rescue => e
152
+ puts "Error Communicating with the Appliance. Please try again later. #{e}"
153
+ return nil
154
+ end
155
+ end
156
+
157
+ private
158
+
159
+
160
+ def add_openstack(name, description,zone, args)
161
+ options = {}
162
+ optparse = OptionParser.new do|opts|
163
+
164
+ opts.on( '-s', '--size SIZE', "Disk Size" ) do |size|
165
+ options[:diskSize] = size.to_l
166
+ end
167
+ opts.on('-i', '--image IMAGE', "Image Name") do |image|
168
+ options[:imageName] = image
169
+ end
170
+
171
+ opts.on('-f', '--flavor FLAVOR', "Flavor Name") do |flavor|
172
+ options[:flavorName] = flavor
173
+ end
174
+ end
175
+ optparse.parse(args)
176
+
177
+ server_payload = {server: {name: name, description: description}, zoneId: zone['id']}
178
+ response = @servers_interface.create(server_payload)
179
+ end
180
+
181
+ def add_standard(name,description,zone, args)
182
+ options = {}
183
+ networkOptions = {name: 'eth0'}
184
+ optparse = OptionParser.new do|opts|
185
+ opts.banner = "Usage: morpheus server add ZONE NAME [options]"
186
+ opts.on( '-u', '--ssh-user USER', "SSH Username" ) do |sshUser|
187
+ options['sshUsername'] = sshUser
188
+ end
189
+ opts.on('-p', '--password PASSWORD', "SSH Password (optional)") do |password|
190
+ options['sshPassword'] = password
191
+ end
192
+
193
+ opts.on('-h', '--host HOST', "HOST IP") do |host|
194
+ options['sshHost'] = host
195
+ end
196
+
197
+ options['dataDevice'] = '/dev/sdb'
198
+ opts.on('-m', '--data-device DATADEVICE', "Data device for LVM") do |device|
199
+ options['dataDevice'] = device
200
+ end
201
+
202
+
203
+ opts.on('-n', '--interface NETWORK', "Default Network Interface") do |net|
204
+ networkOptions[:name] = net
205
+ end
206
+
207
+ opts.on_tail(:NONE, "--help", "Show this message") do
208
+ puts opts
209
+ exit
210
+ end
211
+ end
212
+ optparse.parse(args)
213
+
214
+ server_payload = {server: {name: name, description: description, zone: {id: zone['id']}}.merge(options), network: networkOptions}
215
+ response = @servers_interface.create(server_payload)
216
+
217
+ end
218
+
219
+ def add_amazon(name,description,zone, args)
220
+ puts "NOT YET IMPLEMENTED"
221
+ end
222
+
223
+ def zone_type_for_id(id)
224
+ # puts "Zone Types #{@zone_types}"
225
+ if !@zone_types.empty?
226
+ zone_type = @zone_types.find { |z| z['id'].to_i == id.to_i}
227
+ if !zone_type.nil?
228
+ return zone_type
229
+ end
230
+ end
231
+ return nil
232
+ end
233
+
234
+
235
+ def find_group_by_name(name)
236
+ group_results = @groups_interface.get(name)
237
+ if group_results['groups'].empty?
238
+ puts "Group not found by name #{name}"
239
+ return nil
240
+ end
241
+ return group_results['groups'][0]
242
+ end
243
+
244
+ def find_zone_by_name(groupId, name)
245
+ zone_results = @zones_interface.get({groupId: groupId, name: name})
246
+ if zone_results['zones'].empty?
247
+ puts "Zone not found by name #{name}"
248
+ return nil
249
+ end
250
+ return zone_results['zones'][0]
251
+ end
252
+
253
+ def find_group_by_id(id)
254
+ group_results = @groups_interface.get(id)
255
+ if group_results['groups'].empty?
256
+ puts "Group not found by id #{id}"
257
+ return nil
258
+ end
259
+ return group_results['groups'][0]
260
+ end
261
+ end
@@ -0,0 +1,5 @@
1
+ module Morpheus
2
+ module Cli
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,204 @@
1
+ # require 'yaml'
2
+ require 'io/console'
3
+ require 'rest_client'
4
+ require 'term/ansicolor'
5
+ require 'optparse'
6
+
7
+
8
+ class Morpheus::Cli::Zones
9
+ include Term::ANSIColor
10
+ def initialize()
11
+ @appliance_name, @appliance_url = Morpheus::Cli::Remote.active_appliance
12
+ @access_token = Morpheus::Cli::Credentials.new(@appliance_name,@appliance_url).request_credentials()
13
+ @zones_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).zones
14
+ @groups_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).groups
15
+ @zone_types = @zones_interface.zone_types['zoneTypes']
16
+ end
17
+
18
+ def handle(args)
19
+ if @access_token.empty?
20
+ print red,bold, "\nInvalid Credentials. Unable to acquire access token. Please verify your credentials and try again.\n\n",reset
21
+ return 1
22
+ end
23
+ if args.empty?
24
+ puts "\nUsage: morpheus zones [list,add,remove] [name]\n\n"
25
+ end
26
+
27
+ case args[0]
28
+ when 'list'
29
+ list(args[1..-1])
30
+ when 'add'
31
+ add(args[1..-1])
32
+ when 'remove'
33
+ remove(args[1..-1])
34
+ else
35
+ puts "\nUsage: morpheus zones [list,add,remove] [name]\n\n"
36
+ end
37
+ end
38
+
39
+ def add(args)
40
+ if args.count < 1
41
+ puts "\nUsage: morpheus zones add [name] --group GROUP --type TYPE\n\n"
42
+ return
43
+ end
44
+ params = {zone_type: 'standard'}
45
+ optparse = OptionParser.new do|opts|
46
+ opts.on( '-g', '--group GROUP', "Group Name" ) do |group|
47
+ params[:group] = group
48
+ end
49
+ opts.on( '-t', '--type TYPE', "Zone Type" ) do |zone_type|
50
+ params[:zone_type] = zone_type
51
+ end
52
+ opts.on( '-d', '--description DESCRIPTION', "Description (optional)" ) do |desc|
53
+ params[:description] = desc
54
+ end
55
+ end
56
+ optparse.parse(args)
57
+ zone = {name: args[0], description: params[:description]}
58
+ if !params[:group].nil?
59
+ group = find_group_by_name(params[:group])
60
+ if !group.nil?
61
+ zone['groupId'] = group['id']
62
+ end
63
+ end
64
+
65
+ if !params[:zone_type].nil?
66
+ zone['zoneType'] = {code:zone_type_code_for_name(params[:zone_type])}
67
+ end
68
+
69
+ begin
70
+ @zones_interface.create(zone)
71
+ rescue => e
72
+ if e.response.code == 400
73
+ error = JSON.parse(e.response.to_s)
74
+ ::Morpheus::Cli::ErrorHandler.new.print_errors(error)
75
+ else
76
+ puts "Error Communicating with the Appliance. Please try again later. #{e}"
77
+ end
78
+ return nil
79
+ end
80
+ list([])
81
+ end
82
+
83
+ def remove(args)
84
+ if args.count < 2
85
+ puts "\nUsage: morpheus zones remove [name] --group GROUP\n\n"
86
+ return
87
+ end
88
+
89
+ params = {}
90
+ optparse = OptionParser.new do|opts|
91
+ opts.on( '-g', '--group GROUP', "Group Name" ) do |group|
92
+ params[:group] = group
93
+ end
94
+ end
95
+ optparse.parse(args)
96
+
97
+ if !params[:group].nil?
98
+ group = find_group_by_name(params[:group])
99
+ if !group.nil?
100
+ params[:groupId] = group['id']
101
+ else
102
+ puts "\nGroup #{params[:group]} not found!"
103
+ return
104
+ end
105
+ else params[:group].nil?
106
+ puts "\nUsage: morpheus zones remove [name] --group GROUP"
107
+ return
108
+ end
109
+
110
+
111
+ begin
112
+ zone_results = @zones_interface.get({name: args[0], groupId: params[:groupId]})
113
+ if zone_results['zones'].empty?
114
+ puts "Zone not found by name #{args[0]}"
115
+ return
116
+ end
117
+ @zones_interface.destroy(zone_results['zones'][0]['id'])
118
+ list([])
119
+ rescue RestClient::Exception => e
120
+ if e.response.code == 400
121
+ error = JSON.parse(e.response.to_s)
122
+ ::Morpheus::Cli::ErrorHandler.new.print_errors(error)
123
+ else
124
+ puts "Error Communicating with the Appliance. Please try again later. #{e}"
125
+ end
126
+ return nil
127
+ end
128
+ end
129
+
130
+ def list(args)
131
+ options = {}
132
+ optparse = OptionParser.new do|opts|
133
+ opts.on( '-g', '--group GROUP', "Group Name" ) do |group|
134
+ options[:group] = group
135
+ end
136
+ end
137
+ optparse.parse(args)
138
+ begin
139
+ params = {}
140
+ if !options[:group].nil?
141
+ group = find_group_by_name(options[:group])
142
+ if !group.nil?
143
+ params['groupId'] = group['id']
144
+ end
145
+ end
146
+
147
+ json_response = @zones_interface.get(params)
148
+ zones = json_response['zones']
149
+ print "\n" ,cyan, bold, "Morpheus Zones\n","==================", reset, "\n\n"
150
+ if zones.empty?
151
+ puts yellow,"No zones currently configured.",reset
152
+ else
153
+ zones.each do |zone|
154
+ print cyan, "= #{zone['name']} (#{zone_type_for_id(zone['zoneTypeId'])}) - #{zone['description']}\n"
155
+ end
156
+ end
157
+ print reset,"\n\n"
158
+
159
+ rescue => e
160
+ puts "Error Communicating with the Appliance. Please try again later. #{e}"
161
+ return nil
162
+ end
163
+ end
164
+
165
+ private
166
+
167
+ def zone_type_for_id(id)
168
+ if !@zone_types.empty?
169
+ zone_type = @zone_types.find { |z| z['id'].to_i == id.to_i}
170
+ if !zone_type.nil?
171
+ return zone_type['name']
172
+ end
173
+ end
174
+ return nil
175
+ end
176
+
177
+ def zone_type_code_for_name(name)
178
+ if !@zone_types.empty?
179
+ zone_type = @zone_types.find { |z| z['name'].downcase == name.downcase}
180
+ if !zone_type.nil?
181
+ return zone_type['code']
182
+ end
183
+ end
184
+ return nil
185
+ end
186
+
187
+ def find_group_by_name(name)
188
+ group_results = @groups_interface.get(name)
189
+ if group_results['groups'].empty?
190
+ puts "Group not found by name #{name}"
191
+ return nil
192
+ end
193
+ return group_results['groups'][0]
194
+ end
195
+
196
+ def find_group_by_id(id)
197
+ group_results = @groups_interface.get(id)
198
+ if group_results['groups'].empty?
199
+ puts "Group not found by id #{id}"
200
+ return nil
201
+ end
202
+ return group_results['groups'][0]
203
+ end
204
+ end
@@ -0,0 +1,21 @@
1
+ require "morpheus/cli/version"
2
+
3
+ module Morpheus
4
+ module Cli
5
+ require 'morpheus/api/api_client'
6
+ require 'morpheus/api/groups_interface'
7
+ require 'morpheus/api/zones_interface'
8
+ require 'morpheus/api/servers_interface'
9
+ require 'morpheus/api/instances_interface'
10
+ require 'morpheus/api/instance_types_interface'
11
+ require 'morpheus/cli/credentials'
12
+ require 'morpheus/cli/error_handler'
13
+ require 'morpheus/cli/remote'
14
+ require 'morpheus/cli/groups'
15
+ require 'morpheus/cli/zones'
16
+ require 'morpheus/cli/servers'
17
+ require 'morpheus/cli/instances'
18
+ require 'morpheus/cli/instance_types'
19
+ # Your code goes here...
20
+ end
21
+ end
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'morpheus/cli/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "morpheus-cli"
8
+ spec.version = Morpheus::Cli::VERSION
9
+ spec.authors = ["David Estes"]
10
+ spec.email = ["davydotcom@gmail.com"]
11
+ spec.summary = "Provides CLI Interface to the Morpheus Public/Private Cloud Appliance"
12
+ spec.description = "Morpheus allows one to manage docker containers and deploy applications on the CLI"
13
+ spec.homepage = "http://www.gomorpheus.com"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.6"
23
+ spec.add_development_dependency "rake"
24
+ spec.add_dependency 'term-ansicolor', '~> 1.3.0'
25
+ spec.add_dependency "rest-client", "~> 1.7"
26
+ spec.add_dependency "filesize"
27
+ spec.add_dependency "table_print"
28
+ end
metadata ADDED
@@ -0,0 +1,153 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: morpheus-cli
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - David Estes
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-07-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: term-ansicolor
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 1.3.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 1.3.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: rest-client
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.7'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.7'
69
+ - !ruby/object:Gem::Dependency
70
+ name: filesize
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: table_print
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Morpheus allows one to manage docker containers and deploy applications
98
+ on the CLI
99
+ email:
100
+ - davydotcom@gmail.com
101
+ executables:
102
+ - morpheus
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - ".gitignore"
107
+ - Gemfile
108
+ - LICENSE.txt
109
+ - README.md
110
+ - Rakefile
111
+ - bin/morpheus
112
+ - lib/morpheus/api/api_client.rb
113
+ - lib/morpheus/api/groups_interface.rb
114
+ - lib/morpheus/api/instance_types_interface.rb
115
+ - lib/morpheus/api/instances_interface.rb
116
+ - lib/morpheus/api/servers_interface.rb
117
+ - lib/morpheus/api/zones_interface.rb
118
+ - lib/morpheus/cli.rb
119
+ - lib/morpheus/cli/credentials.rb
120
+ - lib/morpheus/cli/error_handler.rb
121
+ - lib/morpheus/cli/groups.rb
122
+ - lib/morpheus/cli/instance_types.rb
123
+ - lib/morpheus/cli/instances.rb
124
+ - lib/morpheus/cli/remote.rb
125
+ - lib/morpheus/cli/servers.rb
126
+ - lib/morpheus/cli/version.rb
127
+ - lib/morpheus/cli/zones.rb
128
+ - morpheus-cli.gemspec
129
+ homepage: http://www.gomorpheus.com
130
+ licenses:
131
+ - MIT
132
+ metadata: {}
133
+ post_install_message:
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ requirements: []
148
+ rubyforge_project:
149
+ rubygems_version: 2.2.2
150
+ signing_key:
151
+ specification_version: 4
152
+ summary: Provides CLI Interface to the Morpheus Public/Private Cloud Appliance
153
+ test_files: []