Class: AethernalAgent::App

Inherits:
Object
  • Object
show all
Includes:
Apt, Errors, Filesystem, Systemd, Utils
Defined in:
lib/aethernal_agent/app.rb

Constant Summary

Constants included from Systemd

Systemd::SYSTEMD_ACTIONS

Instance Attribute Summary collapse

Attributes included from Errors

#global_errors

Instance Method Summary collapse

Methods included from Errors

#add_errors, #get_errors

Methods included from Utils

#apt_running?, #check_manifest, #container_domain, #current_xdg_runtime_dir, #docker_container_name, #get_current_uid, #get_global_config, #global_config_path, #port_is_free, #prepare_test_config, #print_system, #random_port, #random_string, #run_as, #run_command, #run_systemd_plain, #set_global_config, #systemd_text_for_service, #ubuntu_release, #wait_on_apt, #wait_on_file, #wait_on_port

Methods included from Systemd

#create_service_file, #get_systemd_status, #method_missing, #parse_systemd_status_text, #reload_systemd_config, #run_user_systemctl, #service_file_path

Methods included from Filesystem

#aethernal_agent_file, #aethernal_agent_folder, #copy, #directory, #file, #file_settings, #files_folder, #files_path, #home_folder_path, #meta_folder, #meta_path, #set_ownership, #set_permissions, #template_path, #templates_folder, #write_template

Methods included from Apt

#add_apt_ppa, #apt_package, #apt_update, included

Constructor Details

#initialize(options = {}) ⇒ App

Returns a new instance of App.



22
23
24
25
26
27
28
29
30
31
32
# File 'lib/aethernal_agent/app.rb', line 22

def initialize(options = {})
  options = HashWithIndifferentAccess.new(options)
  options[:ubuntu_version] = ubuntu_release

  self.global_options = options
  self.plugin_path = options[:file_path] || caller_locations(0)[1].path
  self.user = options[:user]

  AethernalAgent.logger.debug("initializing plugin from #{plugin_path}")
  self.manifest = OpenStruct.new(YAML.load_file(File.join(File.dirname(self.plugin_path), "manifest.yml")))
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method in the class AethernalAgent::Systemd

Instance Attribute Details

#container_settingsObject

Returns the value of attribute container_settings.



20
21
22
# File 'lib/aethernal_agent/app.rb', line 20

def container_settings
  @container_settings
end

#global_optionsObject

Returns the value of attribute global_options.



20
21
22
# File 'lib/aethernal_agent/app.rb', line 20

def global_options
  @global_options
end

#manifestObject

Returns the value of attribute manifest.



20
21
22
# File 'lib/aethernal_agent/app.rb', line 20

def manifest
  @manifest
end

#plugin_pathObject

Returns the value of attribute plugin_path.



20
21
22
# File 'lib/aethernal_agent/app.rb', line 20

def plugin_path
  @plugin_path
end

#userObject

Returns the value of attribute user.



20
21
22
# File 'lib/aethernal_agent/app.rb', line 20

def user
  @user
end

Instance Method Details

#app_path(path = nil) ⇒ Object



315
316
317
318
319
320
321
322
# File 'lib/aethernal_agent/app.rb', line 315

def app_path(path=nil)
  full_path = home_folder_path(File.join("apps", self.manifest.package['folder_name']))
  if path
    return File.join(full_path, path)
  else
    return full_path
  end
end

#configure_app_user(options = {}) {|opts| ... } ⇒ Object

Yields:

  • (opts)


134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/aethernal_agent/app.rb', line 134

def configure_app_user(options={})
  errors = {}
  opts, errors = ensure_action_options(__method__,options)
  return {errors: errors} unless errors.empty?

  if self.manifest.apache_configuration
    apache = AethernalAgent::Apache.new(opts)
    errors = apache.ensure_base_config(opts)
    apache.write_app_config(template_path(self.manifest.apache_configuration), self.manifest.plain_name, opts[:port])
  end

  if self.is_docker_install?
    begin
      docker_group = Etc.getgrnam('docker')
    rescue ArgumentError
      self.add_errors("Docker group not found, please install Docker (https://docs.docker.com/engine/install/ubuntu/). If Docker is already installed make sure the docker group exists.") and return create_return_args(opts)
    end

    unless docker_group.mem.include?(self.user)
      self.add_errors("#{self.user} is not a member of the Docker group. Because of the implied security implications aethernal-agent does not do this by default. Please use `usermod -aG docker #{self.user}` if you want to give this user Docker access and restart your Capsule so SystemD has access as well.") and return create_return_args(opts)
    end
    self.container_settings = AethernalAgent::Docker::ContainerSettings.build_from_options(self.manifest["actions"]["configure_app_user"], opts)
  end

  AethernalAgent.logger.debug("Running custom plugin code.")
  yield opts if block_given?
  AethernalAgent.logger.debug("Done running custom plugin code.")

  if self.is_docker_install?
    # We expect the image to be installed here by the install method
    image = self.manifest.package['docker']['image']
    begin
      container = self.container_settings.create_container(self.docker_container_name, image)
      AethernalAgent.logger.debug("Starting: #{container.start}")
    rescue StandardError => e
      self.add_errors(e)
      return create_return_args(opts)
    end
  end

  AethernalAgent.logger.debug("Starting service generation.")
  if self.manifest.services.present?
    self.manifest.services.each do |s|
      options = {service_name: "#{s}.service", user: self.user, vars: opts}
      service_template = template_path("#{s}.service.erb")
      if File.exists?(service_template)
        AethernalAgent.logger.debug("Found service template: '#{service_template}'")
        options[:template] = "#{s}.service.erb"
      elsif File.exists?(files_path("#{s}.service"))
        AethernalAgent.logger.debug("Found service file")
        options[:file] = "#{s}.service"
      end
      create_service_file(options)
    end

    AethernalAgent.logger.debug("Done with service generation.")
  else
    AethernalAgent.logger.debug("Skipping service generation, no service file defined.")
  end

  AethernalAgent.logger.debug("Done with service generation.")
  directory(home_folder_path("apps"), owner: self.user)
  AethernalAgent.logger.debug("Done running configure_app_user")

  return create_return_args(opts)
end

#create_return_args(options) ⇒ Object



236
237
238
239
240
241
242
243
244
# File 'lib/aethernal_agent/app.rb', line 236

def create_return_args(options)
  res = {options: options, errors: get_errors}

  if options[:errors].present?
    res[:errors] << options.slice(:errors)
  end

  return res
end

#ensure_action_options(method_name, options = {}) ⇒ Object



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/aethernal_agent/app.rb', line 260

def ensure_action_options(method_name, options={})
  options = HashWithIndifferentAccess.new(options.reverse_merge(global_options))

  errors = {}
  AethernalAgent.logger.debug "Running '#{method_name}' with options '#{options}'"
  if self.manifest.actions.keys.include?(method_name.to_s)
    opts = self.manifest.actions[method_name.to_s]
    opts.each do |opt, reqs|
      AethernalAgent.logger.debug "Checking option #{opt} - requirements: #{reqs}"

      next if reqs.blank?

      # The option is required but has not been supplied
      if reqs.include?("required") && reqs["required"] == true
        AethernalAgent.logger.debug("#{opt} is required")
        if options.has_key?(opt) && !options[opt].blank?
          AethernalAgent.logger.debug("required option is supplied")
        else
          AethernalAgent.logger.debug("required option is not supplied")
          errors[opt] ||= []
          errors[opt] << "is required but has not been supplied"
          next
        end
      end

      # This option should be an autogenerated string (passwords)
      if reqs.include?("auto_generate") && reqs["auto_generate"] == true && (!options.has_key?(opt) || options[opt].blank?)
        options[opt] = self.random_string
      end

      # This option should be an autogenerated port
      if reqs.include?("auto_generate_port") && reqs["auto_generate_port"] == true && (!options.has_key?(opt) || options[opt].blank?)
        min = reqs["min"] || 2000
        max = reqs["max"] || 8000
        options[opt] = random_port(min..max)
      end

      # The option should be a linux_user
      if reqs.include?("linux_user") && reqs["linux_user"] == true
        AethernalAgent.logger.debug "Checking to see if '#{options[:user]}' is a local linux user."
        begin
          Etc.getpwnam(options[:user])
        rescue ArgumentError => e
          errors[opt] ||= []
          errors[opt] << e.to_s
        end
      end
    end
  else
    puts "Method '#{method_name}' not defined in manifest."
  end

  return options, errors
end

#extract(source_file, options = {}) ⇒ Object



343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/aethernal_agent/app.rb', line 343

def extract(source_file, options = {})
  options.reverse_merge!(extract_to_path: self.app_path, source_folder: self.app_path, auto_detect: true, extract_as: File.extname(source_file))

  extract_from = File.join(options[:source_folder], source_file)
  extract_to = options[:extract_to_path]

  case options[:extract_as].gsub('.', '')
  when "gz", "tar"
    run_command("tar -C #{extract_to} -xvf #{extract_from}")
  when "tgz"
    run_command("tar -C #{extract_to} -zxvf #{extract_from}")
  when "bz2", "tar"
    run_command("tar -C #{extract_to} -xvjf #{extract_from}")
  when "zip"
    run_command("unzip -od #{extract_to} #{extract_from}")
  else
    raise "#{options[:extract_as]} not implemented yet"
  end

  file(extract_from, action: :delete) if options[:delete_after]
end

#icon_file(path = nil) ⇒ Object



332
333
334
335
336
337
338
339
340
341
# File 'lib/aethernal_agent/app.rb', line 332

def icon_file(path=nil)
  if self.manifest.icon.present?
    icon_path = meta_path(self.manifest.icon['name'])
    if File.exist?(icon_path)
      return Base64.encode64(File.read(icon_path))
    end
  end

  return nil
end

#icon_shaObject



324
325
326
327
328
329
330
# File 'lib/aethernal_agent/app.rb', line 324

def icon_sha
  if self.manifest.icon.present?
    return self.manifest.icon["sha256sum"]
  else
    return ""
  end
end

#install_packages(options = {}) ⇒ Object

FIXME: Add error tracking



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
# File 'lib/aethernal_agent/app.rb', line 35

def install_packages(options = {})
  directory(home_folder_path("/.config"), owner: self.user)
  AethernalAgent.logger.info("Installing packages for #{self.manifest.name} - v#{self.manifest.version}")

  if self.manifest.package.has_key?('ppa')
    self.manifest.package['ppa'].each do |ppa|
      AethernalAgent.logger.debug("Setting up PPA: #{ppa}")
      run_command("add-apt-repository ppa:#{ppa} -y")
    end
  end

  self.manifest.package.each_key do |package_type|
    case package_type
    when 'apt'
      AethernalAgent.logger.debug("apt options: #{self.manifest.package['apt']} for ubuntu version #{ubuntu_release}")
      options = self.manifest.package['apt'][ubuntu_release]
      options = self.manifest.package['apt']["all"] if self.manifest.package['apt']["all"].present?

      AethernalAgent.logger.debug("Matched options:  #{options}")
      packages = options['packages']

      if options.has_key?('add_sources')
        AethernalAgent.logger.debug("Adding custom sources")
        options['add_sources'].each do |name, source|
          AethernalAgent.logger.debug("Setting up custom apt source '#{name}'")
          file_path = "/etc/apt/sources.list.d/#{name}.list"

          unless File.exist?(file_path)
            run_command("echo '#{source['source_url']}' > #{file_path}")
          else
            AethernalAgent.logger.debug("Custom apt source '#{name}' already present, not rewriting.")
          end
          run_command("curl #{source['key_url']} | sudo apt-key add -") if source['key_url'].present?
          run_command("sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys #{source['key_id']}") if source['key_id'].present?
        end
        apt_update
      end

      if packages
        AethernalAgent.logger.debug("Installing packages #{packages}")
        apt_package(packages: packages)
      end
    when 'direct_download'
      opts = self.manifest.package[package_type]
      opts = self.manifest.package[package_type][ubuntu_release] if self.manifest.package[package_type][ubuntu_release].present?

      target_file = File.join(app_path, opts["target_name"])
      directory(app_path, owner: self.user)
      AethernalAgent.logger.debug("Downloading file from '#{opts["url"]}' to '#{target_file}'")

      File.open(target_file, "wb") do |saved_file|
        open(opts["url"], "rb") do |read_file|
          saved_file.write(read_file.read)
        end
      end

      if opts["auto_unzip"]
        unzip(opts["target_name"], delete_after: true)
      elsif opts["auto_extract"]
        extract(opts["target_name"], delete_after: true)
      end
    when 'git'
      run_command("mkdir #{home_folder_path(File.join('apps'))}")
      run_command("mkdir #{app_path}")
      run_command("git clone #{self.manifest['package']['git']['url']} #{File.join(app_path)}")
    when 'docker'
      options = self.manifest.package['docker']
      AethernalAgent.logger.info("Pulling Docker image #{options['image']}")
      run_command("docker pull #{options['image']}")
    end
  end
  AethernalAgent.logger.info("Done Installing packages for #{self.manifest.name} - v#{self.manifest.version}")
  return create_return_args(options)
end

#is_docker_install?Boolean

Returns:

  • (Boolean)


373
374
375
# File 'lib/aethernal_agent/app.rb', line 373

def is_docker_install?
  self.manifest.installation_type == 'docker_image'
end

#is_local_install?Boolean

Returns:

  • (Boolean)


369
370
371
# File 'lib/aethernal_agent/app.rb', line 369

def is_local_install?
  self.manifest.installation_type == 'local_install'
end

#remove_app_user(options = {}) {|opts| ... } ⇒ Object

Yields:

  • (opts)


201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/aethernal_agent/app.rb', line 201

def remove_app_user(options={})
  opts, errors = ensure_action_options(__method__,options)
  return {errors: errors} unless errors.empty?

  run_user_systemctl(:stop)

  if self.manifest.services.present?
    self.manifest.services.each do |service|
      file(service_file_path("#{service}.service"), action: :delete)
    end
  end

  if self.manifest.apache_configuration
    apache = AethernalAgent::Apache.new(opts)
    apache.remove_app_config(self.manifest.plain_name)
  end

  # Jackett is removing necessary packages, for now dont uninstall a package ever in user_configure
  #if self.is_local_install?
  #  self.uninstall_packages(opts)
  #end
  if self.is_docker_install?
    begin
      container = ::Docker::Container.get(self.docker_container_name)
      container.delete(:force => true)
    rescue ::Docker::Error::NotFoundError
      AethernalAgent.logger.warn("Wanted to delete Docker container '#{self.docker_container_name}' but it did not exist.")
    end
  end

  yield opts if block_given?

  return create_return_args(opts)
end

#sha_1_hash(password, salt) ⇒ Object



377
378
379
380
381
382
383
# File 'lib/aethernal_agent/app.rb', line 377

def sha_1_hash(password, salt)
  sha = Digest::SHA1.new
  sha.update(salt)
  sha.update(password)

  return sha.hexdigest
end

#status(options = {}) ⇒ Object



246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/aethernal_agent/app.rb', line 246

def status(options = {})
  if options[:service_name]
    status = get_systemd_status(options[:service_name])
    return status.to_h if status
  else
    stati = self.manifest.services.collect do |service|
      status = get_systemd_status(service)
      status.to_h if status
    end

    return stati
  end
end

#uninstall_packages(options = {}) ⇒ Object



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/aethernal_agent/app.rb', line 110

def uninstall_packages(options = {})
  AethernalAgent.logger.info("Uninstalling packages for #{self.manifest.name} - v#{self.manifest.version}")

  self.stop
  self.disable

  self.manifest.package.each_key do |package_type|
    case package_type
    when "apt"
      options = self.manifest.package['apt'][ubuntu_release]
      AethernalAgent.logger.debug("Using apt for ubuntu #{ubuntu_release} - #{options}")
      packages = options['packages']
      if packages
        AethernalAgent.logger.debug("Removing packages #{packages}")
        apt_package(packages: packages, action: :remove)
      end
    when "direct_download"
      directory(app_path, action: :delete)
    end
  end

  return create_return_args(options)
end

#unzip(source_file) ⇒ Object



365
366
367
# File 'lib/aethernal_agent/app.rb', line 365

def unzip(source_file)
  extract(source_file, extract_as: "zip")
end