class Chef::Provider::Package::Snap

Public Instance Methods

candidate_version() click to toggle source
# File lib/chef/provider/package/snap.rb, line 53
def candidate_version
  package_name_array.each_with_index.map do |pkg, i|
    available_version(i)
  end
end
define_resource_requirements() click to toggle source
# File lib/chef/provider/package/snap.rb, line 43
def define_resource_requirements
  requirements.assert(:install, :upgrade, :remove, :purge) do |a|
    a.assertion { !new_resource.source || ::File.exist?(new_resource.source) }
    a.failure_message Chef::Exceptions::Package, "Package #{new_resource.package_name} not found: #{new_resource.source}"
    a.whyrun "assuming #{new_resource.source} would have previously been created"
  end

  super
end
get_current_versions() click to toggle source
# File lib/chef/provider/package/snap.rb, line 59
def get_current_versions
  package_name_array.each_with_index.map do |pkg, i|
    installed_version(i)
  end
end
install_package(names, versions) click to toggle source
# File lib/chef/provider/package/snap.rb, line 65
def install_package(names, versions)
  if new_resource.source
    install_snap_from_source(names, new_resource.source)
  else
    resolved_names = names.each_with_index.map { |name, i| available_version(i).to_s unless name.nil? }
    install_snaps(resolved_names)
  end
end
load_current_resource() click to toggle source
# File lib/chef/provider/package/snap.rb, line 35
def load_current_resource
  @current_resource = Chef::Resource::SnapPackage.new(new_resource.name)
  current_resource.package_name(new_resource.package_name)
  current_resource.version(get_current_versions)

  current_resource
end
purge_package(names, versions)
Alias for: remove_package
remove_package(names, versions) click to toggle source
# File lib/chef/provider/package/snap.rb, line 83
def remove_package(names, versions)
  resolved_names = names.each_with_index.map { |name, i| installed_version(i).to_s unless name.nil? }
  uninstall_snaps(resolved_names)
end
Also aliased as: purge_package
upgrade_package(names, versions) click to toggle source
# File lib/chef/provider/package/snap.rb, line 74
def upgrade_package(names, versions)
  if new_resource.source
    install_snap_from_source(names, new_resource.source)
  else
    resolved_names = names.each_with_index.map { |name, i| available_version(i).to_s unless name.nil? }
    update_snaps(resolved_names)
  end
end

Private Instance Methods

available_version(index) click to toggle source

@return Array<Version>

# File lib/chef/provider/package/snap.rb, line 93
def available_version(index)
  @available_version ||= []

  @available_version[index] ||= if new_resource.source
                                  get_snap_version_from_source(new_resource.source)
                                else
                                  get_latest_package_version(package_name_array[index], new_resource.channel)
                                end

  @available_version[index]
end
call_snap_api(method, uri, post_data = nil?) click to toggle source

ToDo: Would prefer to use net/http over socket

# File lib/chef/provider/package/snap.rb, line 127
def call_snap_api(method, uri, post_data = nil?)
  request = "#{method} #{uri} HTTP/1.0\r\n" +
    "Accept: application/json\r\n" +
    "Content-Type: application/json\r\n"
  if method == "POST"
    request.concat("Content-Length: #{post_data.bytesize}\r\n\r\n#{post_data}")
  end
  request.concat("\r\n")
  # While it is expected to allow clients to connect using HTTPS over a TCP socket,
  # at this point only a UNIX socket is supported. The socket is /run/snapd.socket
  # Note - UNIXSocket is not defined on windows systems
  if defined?(::UNIXSocket)
    UNIXSocket.open("/run/snapd.socket") do |socket|
      # Send request, read the response, split the response and parse the body
      socket.print(request)
      response = socket.read
      headers, body = response.split("\r\n\r\n", 2)
      JSON.parse(body)
    end
  end
end
generate_multipart_form_data(snap_name, action, options, path, content_length) click to toggle source

Constructs the multipart/form-data required to sideload packages github.com/snapcore/snapd/wiki/REST-API#sideload-request

@param snap_name [String] An array of snap package names to install
@param action [String] The action. Valid: install or try
@param options [Hash] Misc configuration Options
@param path [String] Path to the package on disk
@param content_length [Integer] byte size of the snap file
# File lib/chef/provider/package/snap.rb, line 238
        def generate_multipart_form_data(snap_name, action, options, path, content_length)
          snap_options = options.map do |k, v|
            <<~SNAP_OPTION
              Content-Disposition: form-data; name="#{k}"

              #{v}
              --#{snap_name}
            SNAP_OPTION
          end

          multipart_form_data = <<~SNAP_S
            Host:
            Content-Type: multipart/form-data; boundary=#{snap_name}
            Content-Length: #{content_length}

            --#{snap_name}
            Content-Disposition: form-data; name="action"

            #{action}
            --#{snap_name}
            #{snap_options.join("\n").chomp}
            Content-Disposition: form-data; name="snap"; filename="#{path}"

            <#{content_length} bytes of snap file data>
            --#{snap_name}
          SNAP_S
          multipart_form_data
        end
generate_snap_json(snap_names, action, channel, options, revision = nil) click to toggle source

Constructs json to post for snap changes

@param snap_names [Array] An array of snap package names to install
@param action [String] The action.  install, refresh, remove, revert, enable, disable or switch
@param channel [String] The release channel.  Ex. stable
@param options [Hash] Misc configuration Options
@param revision [String] A revision/version
# File lib/chef/provider/package/snap.rb, line 274
def generate_snap_json(snap_names, action, channel, options, revision = nil)
  request = {
      "action" => action,
      "snaps" => snap_names,
  }
  if %w{install refresh switch}.include?(action)
    request["channel"] = channel
  end

  # No defensive handling of params
  # Snap will throw the proper exception if called improperly
  # And we can provide that exception to the end user
  request["classic"] = true if options["classic"]
  request["devmode"] = true if options["devmode"]
  request["jailmode"] = true if options["jailmode"]
  request["revision"] = revision unless revision.nil?
  request["ignore_validation"] = true if options["ignore-validation"]
  request
end
get_change_id(id) click to toggle source
# File lib/chef/provider/package/snap.rb, line 149
def get_change_id(id)
  call_snap_api("GET", "/v2/changes/#{id}")
end
get_id_from_async_response(response) click to toggle source
# File lib/chef/provider/package/snap.rb, line 153
def get_id_from_async_response(response)
  if response["type"] == "error"
    raise "status: #{response["status"]}, kind: #{response["result"]["kind"]}, message: #{response["result"]["message"]}"
  end
  response["change"]
end
get_installed_package_by_name(name) click to toggle source
# File lib/chef/provider/package/snap.rb, line 335
def get_installed_package_by_name(name)
  json = call_snap_api("GET", "/v2/snaps/#{name}")
  # We only allow 200 or 404s
  unless [200, 404].include? json["status-code"]
    raise Chef::Exceptions::Package, json["result"], caller
  end
  json["result"]
end
get_installed_package_conf(name) click to toggle source
# File lib/chef/provider/package/snap.rb, line 344
def get_installed_package_conf(name)
  json = call_snap_api("GET", "/v2/snaps/#{name}/conf")
  json["result"]
end
get_installed_package_version_by_name(name) click to toggle source
# File lib/chef/provider/package/snap.rb, line 325
def get_installed_package_version_by_name(name)
  result = get_installed_package_by_name(name)
  # Return nil if not installed
  if result["status-code"] == 404
    nil
  else
    result["version"]
  end
end
get_installed_packages() click to toggle source
# File lib/chef/provider/package/snap.rb, line 316
def get_installed_packages
  json = call_snap_api("GET", "/v2/snaps")
  # We only allow 200 or 404s
  unless [200, 404].include? json["status-code"]
    raise Chef::Exceptions::Package, json["result"], caller
  end
  json["result"]
end
get_latest_package_version(name, channel) click to toggle source
# File lib/chef/provider/package/snap.rb, line 306
def get_latest_package_version(name, channel)
  json = call_snap_api("GET", "/v2/find?name=#{name}")
  if json["status-code"] != 200
    raise Chef::Exceptions::Package, json["result"], caller
  end

  # Return the version matching the channel
  json["result"][0]["channels"]["latest/#{channel}"]["version"]
end
get_snap_version_from_source(path) click to toggle source
# File lib/chef/provider/package/snap.rb, line 188
def get_snap_version_from_source(path)
  body = {
      "context-id" => "get_snap_version_from_source_#{path}",
      "args" => ["info", path,],
  }.to_json

  # json = call_snap_api('POST', '/v2/snapctl', body)
  response = snapctl(["info", path])
  Chef::Log.trace(response)
  response.error!
  get_version_from_stdout(response.stdout)
end
get_version_from_stdout(stdout) click to toggle source
# File lib/chef/provider/package/snap.rb, line 201
def get_version_from_stdout(stdout)
  stdout.match(/version: (\S+)/)[1]
end
install_snap_from_source(name, path) click to toggle source
# File lib/chef/provider/package/snap.rb, line 205
def install_snap_from_source(name, path)
  # json = call_snap_api('POST', '/v2/snapctl', body)
  response = snapctl(["install", path])
  Chef::Log.trace(response)
  response.error!
end
install_snaps(snap_names) click to toggle source
# File lib/chef/provider/package/snap.rb, line 212
def install_snaps(snap_names)
  response = post_snaps(snap_names, "install", new_resource.channel, new_resource.options)
  id = get_id_from_async_response(response)
  wait_for_completion(id)
end
installed_version(index) click to toggle source

@return [Array<Version>]

# File lib/chef/provider/package/snap.rb, line 106
def installed_version(index)
  @installed_version ||= []
  @installed_version[index] ||= get_installed_package_version_by_name(package_name_array[index])
  @installed_version[index]
end
post_snaps(snap_names, action, channel, options, revision = nil) click to toggle source

Post to the snap api to update snaps

@param snap_names [Array] An array of snap package names to install
@param action [String] The action.  install, refresh, remove, revert, enable, disable or switch
@param channel [String] The release channel.  Ex. stable
@param options [Hash] Misc configuration Options
@param revision [String] A revision/version
# File lib/chef/provider/package/snap.rb, line 301
def post_snaps(snap_names, action, channel, options, revision = nil)
  json = generate_snap_json(snap_names, action, channel, options, revision = nil)
  call_snap_api("POST", "/v2/snaps", json)
end
safe_version_array() click to toggle source
# File lib/chef/provider/package/snap.rb, line 112
def safe_version_array
  if new_resource.version.is_a?(Array)
    new_resource.version
  elsif new_resource.version.nil?
    package_name_array.map { nil }
  else
    [new_resource.version]
  end
end
set_installed_package_conf(name, value) click to toggle source
# File lib/chef/provider/package/snap.rb, line 349
def set_installed_package_conf(name, value)
  response = call_snap_api("PUT", "/v2/snaps/#{name}/conf", value)
  id = get_id_from_async_response(response)
  wait_for_completion(id)
end
snapctl(*args) click to toggle source
# File lib/chef/provider/package/snap.rb, line 184
def snapctl(*args)
  shell_out!("snap", *args)
end
uninstall_snaps(snap_names) click to toggle source
# File lib/chef/provider/package/snap.rb, line 224
def uninstall_snaps(snap_names)
  response = post_snaps(snap_names, "remove", new_resource.channel, new_resource.options)
  id = get_id_from_async_response(response)
  wait_for_completion(id)
end
update_snaps(snap_names) click to toggle source
# File lib/chef/provider/package/snap.rb, line 218
def update_snaps(snap_names)
  response = post_snaps(snap_names, "refresh", new_resource.channel, new_resource.options)
  id = get_id_from_async_response(response)
  wait_for_completion(id)
end
wait_for_completion(id) click to toggle source
# File lib/chef/provider/package/snap.rb, line 160
def wait_for_completion(id)
  n = 0
  waiting = true
  while waiting
    result = get_change_id(id)
    puts "STATUS: #{result["result"]["status"]}"
    case result["result"]["status"]
    when "Do", "Doing", "Undoing", "Undo"
      # Continue
    when "Abort"
      raise result
    when "Hold", "Error"
      raise result
    when "Done"
      waiting = false
    else
      # How to handle unknown status
    end
    n += 1
    raise "Snap operating timed out after #{n} seconds." if n == 300
    sleep(1)
  end
end